Files
mesh-controller/internal/identity/migrations/0001-signing-key.sql
T
jschoubben 7553af6c5a The control plane's signing key, and a second context to hold it
Everything is blocked on what a node presents to prove which node it is. This
builds the other direction, which is not blocked: what a node believes.

identity is the second of the seven contexts. It holds an Ed25519 signing key
the control plane generates once, whose public half now travels in every
enrolment token. A node believes a declaration because it carries a signature
that key made -- pinning only the broker would make the control plane's
authority transitive, and since the host applies whatever the link delivers, a
compromised broker forging declarations is the whole machine.

Establishing the key is idempotent, and it has to be: a second key generated by
a restart is a mesh where every node holds the wrong public half, so every
declaration is refused by every node with nothing visibly wrong. The guarantee
is a partial unique index plus a read-back, not the check before the insert --
six processes racing to establish all agree on one key, and there is a test
that runs them.

Tokens are now one line of base64 carrying three of their four parts. The
missing two are the broker's address and its certificate fingerprint, both step
5 of the bootstrap. The command prints the token and names what is missing
rather than emitting something that looks usable.

The second context also tests a claim this repository had made and never
checked: that a context reaches only its own store. Two databases, two
credentials, no setting that reaches both. Running migrate with one stops and
names the grant it lacks -- verified, not asserted. Assembling a token needs a
node record from one and a key from the other, and neither reads the other's
store; the process holding both grants asks each for its part.

45 tests, none skipped. Fault injection found one test whose property is
enforced somewhere other than where I injected -- idempotency comes from the
database constraint, not from the early return, which is what the code comment
already said.
2026-08-29 15:05:23 +02:00

31 lines
1.7 KiB
SQL

-- The control plane's own signing identity.
--
-- novox/hq ADR 0004: a node takes instruction from the control plane behind the broker, and each
-- declaration is verified by its signature, every time. The public half of this key travels in
-- every enrolment token; the private half never leaves this context.
--
-- Why not pin only the broker: that would make the control plane's authority transitive. A
-- compromised broker could then forge declarations, and since the host applies whatever the link
-- delivers, that is the whole machine. The transport is verified once at connect; the instruction
-- is verified on arrival.
create table signing_key (
id uuid primary key default gen_random_uuid(),
-- Ed25519. Fixed rather than a column: a key that carries its own algorithm invites a caller
-- to be told which one to use, and the two sizes below are Ed25519's.
public bytea not null check (octet_length(public) = 32),
private bytea not null check (octet_length(private) = 64),
created timestamptz not null default now(),
-- Retiring a signing key is a fleet-wide operation with an overlapping rollover -- every node
-- holds the public half, delivered in a token it may have received months ago. So keys are
-- retired, never deleted, and more than one may be valid at a time during a rollover.
retired timestamptz
);
-- The rollover is what makes this a partial index rather than a plain unique constraint: exactly
-- one key may be signing at any moment, while any number of retired ones remain verifiable.
create unique index signing_key_one_active on signing_key ((retired is null)) where retired is null;