Commit Graph
4 Commits
Author SHA1 Message Date
jschoubben 38d4e77cec A certificate is issued once and kept
Every signing carries a fresh random serial, so a mesh that signed per
composition composed a different declaration every time it was asked
what a machine should be. Every machine carrying a certificate then
stood eternally "waiting" — pushed seconds ago and already behind — and
the forge test, the first to wait for settledness on such a machine,
failed four runs in a row wearing three other faults' clothes.

Found live on a kept mesh, which is what settled it: two plans seconds
apart, identical to the byte but for one serial, in the certificate
file. Deduction had four theories; the diff had one line.

The keeping columns had existed since the serving key's migration —
"and what was issued for it" — and were written by nothing, the same
shape ReleasePorts was found in this morning.

Kept beside the serving key it certifies, and it stands while the name,
the key and the clock agree: a node rejoining with a new key or renamed
gets a fresh signing, exactly as if nothing were kept, and so does one
whose certificate is into its last stretch of life. The port's rule and
the secret's, applied to the third thing composed fresh each time.
2026-09-01 22:26:50 +02:00
jschoubben 646609c1b2 The mesh certifies names inside it
08-connectivity keeps two authorities apart on purpose: a public one for
names the outside world reaches, and the mesh's own for names only the
mesh knows. Nothing implemented the second, so anything between machines
was plaintext or trust-on-first-use — which the design refuses everywhere
else.

A node now generates a fourth key at enrolment and reports the public
half. A fourth, because a key used for two purposes is one rotation away
from breaking the other: the identity key signs messages to the mesh and
would do for TLS, and reusing it would mean rotating a node's identity
every time its certificate is replaced.

**Nothing secret travels and nothing is sealed.** A certificate authority
says "this name belongs to the holder of this key", so the mesh signs a
public half it cannot use, and the certificate it issues is public. A
module asks for one and is given the certificate and, if it wants,
the mesh's own — the private key is a path to a file the machine already
has, the same arrangement the private network's key uses.

Asserted by verifying rather than inspecting, because a certificate that
parses and does not chain fails at the moment something connects:

- what the mesh issues verifies against the mesh, for the name asked for
- the name is in the subject alternative names, since a certificate
  carrying it only in the common name is refused by every modern client
- it certifies the key the node generated and no other
- another mesh's certificate does not verify, which is the whole point of
  two authorities being separate
- the authority cannot sign another authority — one that could is one
  that can be delegated without anybody deciding to
- two control planes starting together agree on one authority, or a mesh
  has certificates half its machines refuse

Certificates last ten years, which is a choice: a short life needs
something to renew it, and a renewal that fails silently is a mesh that
stops trusting itself on a date nobody wrote down. What makes one
replaceable is that the mesh reissues on demand, not that it expires.
2026-08-31 00:09:13 +02:00
jschoubben 6d3bb18546 A node is known by a key it generated
The thing I had been calling blocked for weeks, built in an afternoon once it
was pointed out that it was already decided. 08-connectivity says of the
overlay keys: each node generates its own keypair, the private half never
leaves the machine, the public half is published -- and says outright this IS
ADR 0004's "a node holds its own identity". Nobody had applied it to node
identity itself.

identity now holds the public half of each node's key. Only the public half,
which is the property worth having: a copy of this database is a list of who to
believe, not a set of credentials, so compromise of a node really is compromise
of only that node.

Exactly one key is live per node, and re-enrolment revokes the one it replaced
in the same transaction -- two live identities is the stolen-laptop case with
the replaced machine still believed.

Fault injection was worth the time here. Three findings. The unique index was
defended by no test at all: sequential enrolment is already safe because the
code revokes before inserting, so removing the constraint changed nothing. The
constraint only matters when two enrolments race, and there is now a test that
runs six at once and fails without it.

My injection harness also lied to me. One injection matched nothing, changed no
file, and reported NO BITE identically to a real one -- so a test that defends
nothing and an injection that does nothing look the same. The harness now
checksums the files and says NO-OP when they did not change.

And one honest NO BITE left standing: making the key lookup return a zero key
for an unknown node does not fail the test, because the signature check refuses
it a line later. Two independent mechanisms, not a placebo.

61 tests, none skipped.
2026-08-29 15:25:19 +02:00
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