Files
jschoubben 66768208d2 Node records, and the right to join once
The next step after the schema: inventory now holds node records and enrolment
tokens, and mesh-control has the commands to work with them.

A token is issued for a node record, which is where re-enrolment gets decided
-- what an identity binds to is settled when the token is made, not when it is
presented, so the machine presenting one does not need to know whether it is
joining or returning.

What the token guarantees, each with a test confirmed to fail when the
behaviour is removed: the secret is 256 random bits, shown once and stored only
as a hash; it works exactly once; it stops working when it expires; issuing
again for a node invalidates the outstanding one, because two live tokens are
two machines able to join as the same node. Redemption is a single statement
that finds and spends together, so eight concurrent attempts on one secret
produce exactly one winner rather than a race between a check and a write.

Refusals are deliberately identical for unknown, spent and expired. Somebody
guessing must not learn which guess was a real token that had merely aged out.

SHA-256 rather than a password hash, and that is a choice not a shortcut: the
secret is high-entropy random, so there is nothing to guess and a slow hash
would buy nothing while making every redemption expensive.

It stops before what a node receives in exchange. What a machine presents
afterwards to prove it is that node is not decided anywhere, and a migration is
the most expensive place here to guess.

So a token carries one of the four things ADR 0004 requires. The command prints
the secret and then says exactly that -- the broker's address, its certificate
fingerprint and the control plane's signing identity do not exist yet. Better
than emitting something that looks complete and silently cannot be used.
2026-08-29 14:46:03 +02:00

39 lines
2.0 KiB
SQL

-- The right to join, once.
--
-- novox/hq ADR 0004: a token carries the broker's address, the fingerprint to expect, the control
-- plane's signing identity, and a one-time secret. Only the last of those is stored here -- the
-- other three are facts about the mesh, the same in every token, and belong wherever the mesh's
-- own configuration lives rather than copied into each row.
create table enrolment_token (
id uuid primary key default gen_random_uuid(),
-- A token is issued FOR a node record, and that is where re-enrolment is decided
-- (novox/hq 09-the-node-lifecycle). The host presenting it does not need to know whether it
-- is joining as a new node or returning as an existing one; what the identity binds to was
-- settled when the token was made.
--
-- Cascading: a node record removed takes its unused tokens with it. A token outliving the
-- record it was issued for is a right to join as nobody.
node uuid not null references node(id) on delete cascade,
-- The secret is never stored. What is stored is a hash of it, so a copy of this table is not
-- a set of working credentials -- the same reason a password is not kept.
--
-- Unique because a collision would make two tokens redeem as one, and because it lets the
-- lookup at redemption be by hash rather than a scan.
secret text not null unique,
issued timestamptz not null default now(),
-- "Useless once used and useless after it expires" is two conditions, so it is two columns.
-- Neither is a status field: a status has to be written by something noticing, and nothing
-- notices a token quietly ageing out. Both are read from what is already here.
expires timestamptz not null,
redeemed timestamptz
);
-- Redemption looks a token up by the hash of what was presented, and it is the one query on the
-- path where a node is waiting.
create index enrolment_token_node on enrolment_token (node);