A node makes its own identity, and checks the broker before speaking

The host side of enrolment. It parses a token the control plane issued, dials
the broker, refuses anything but the pinned certificate, and generates an
Ed25519 keypair whose private half never leaves the machine.

Verified against a real LavinMQ serving a real certificate: the pin matched and
the node proceeded. Then against a second broker with a different certificate
on another port, which was refused -- with an error that says retrying will not
help, because it does not mean the network is down, it means the mesh was
substituted.

InsecureSkipVerify is set and that is the point rather than a weakening. At
bootstrap the broker is self-signed and reached at an address, so there is no
authority to trace and no name to match. Chain and hostname checks are replaced
with something stricter: this exact certificate or nothing, checked in
VerifyPeerCertificate, which runs before the handshake completes -- so nothing
is sent to the wrong broker. There is a test that counts the bytes an impostor
receives, and it is zero.

The token format is defined separately here and in the control plane, because
this binary requires nothing present and does not import it. They are held
together by a test on each side asserting the exact field names, so a rename
breaks both immediately rather than at enrolment on a real machine.

Two distinctions the identity file has to keep. A machine that never joined has
no identity, which is an ordinary state and not a fault. A machine whose
identity cannot be read is a different thing entirely, and must not take the
same path -- re-enrolling would discard the identity the mesh still believes and
need a person with a new token. Fault injection found the second case untested:
the corrupt-file test was passing on the parse check, so the read-error path had
nothing defending it. It does now.

An already-enrolled machine refuses to enrol again rather than quietly
acquiring a second identity.

What is not built is the link. Enrolment stops after verifying the broker and
generating the identity, having saved nothing, so it can be run again unchanged.

132 tests, plus 32 launcher and 9 rollback.
This commit is contained in:
2026-08-29 15:38:19 +02:00
parent a740959cb0
commit 65d896d96e
7 changed files with 860 additions and 5 deletions
+26
View File
@@ -20,6 +20,32 @@ the one.
**It does not decide.** Anything needing knowledge of another node is the control plane's, and
the host never queries the mesh database. It receives declarations and applies them.
## Joining a mesh
```
mesh-host enrol --token <token> --name <what this machine is called>
```
**The node generates its own identity** — an Ed25519 keypair whose private half never leaves the
machine. The mesh records the public half. Nothing is issued to this node; it arrives holding its
identity, and what it receives is being known.
**The broker's certificate is checked before this machine sends anything.** The token pins a
fingerprint; the connection is refused if what answers presents anything else. That refusal has
its own error and says plainly that retrying will not help, because it does not mean the network
is down — it means the mesh was substituted, and since this host applies whatever the link
delivers, that would be the whole machine.
There is no certificate authority involved and no hostname check. At bootstrap the broker is
self-signed and reached at an address rather than a name, so there is nothing to trace and nothing
to match. One exact certificate, or nothing, which is stricter than either.
**An already-enrolled machine refuses to enrol again.** The mesh believes its first identity, so
replacing it is deliberate: remove the identity file first.
**What is not built is the link itself.** Enrolment verifies the broker and generates the identity,
and then stops, having saved nothing — so it can be run again unchanged.
## What exists today
**Stages 1 and 2.** It reports what a machine is, and it applies a declaration to one. It