Mesh state is not bound to the issuing coordinator (re-enroll never converges) #325

Open
opened 2026-11-03 12:32:35 +00:00 by kayg · 1 comment
Owner

Found by J80c on #265 (2026-11-03). It affects Linux and macOS nodes.

The saved mesh state is not bound to the coordinator that issued it. A node that re-enrolls against a different coordinator keeps its old state, rejects every update from the new coordinator, and never converges.

Expected: the saved state records the issuing coordinator's identity (its CA key). Re-enrolling with a different coordinator either replaces the state atomically or refuses with a clear message. Updates signed by any other coordinator are rejected.

J135's security review of #265 is checking whether this is also a security issue, beyond convergence.

Found by J80c on #265 (2026-11-03). It affects Linux and macOS nodes. The saved mesh state is not bound to the coordinator that issued it. A node that re-enrolls against a different coordinator keeps its old state, rejects every update from the new coordinator, and never converges. Expected: the saved state records the issuing coordinator's identity (its CA key). Re-enrolling with a different coordinator either replaces the state atomically or refuses with a clear message. Updates signed by any other coordinator are rejected. J135's security review of #265 is checking whether this is also a security issue, beyond convergence.
Author
Owner

J80d (#265 round 2): macOS is done on the branch, Linux stays here.

macOS, macos-node-265-s1 commit fd959b443 ("bind Mac mesh state to the enrolment that issued it"):

  • mesh-public.json records a binding: sha256 of the issuing coordinator's CA certificate, the host id, and sha256 of the infraternal agent's mTLS public key. These are all public values from identity.json, built in remote::run.
  • On restore, state that another enrolment issued loses its config, routes and interface, and the WireGuard key file is removed. The node registers a new key.
  • Unbound state from slice-1 builds is adopted once.
  • agent join (root, macOS) removes mesh-public.json and the key after a successful enrolment.
  • The leaf certificate is deliberately not part of the binding, because it renews every 3.5 days.

Linux is not included. The same rule exists in node.rs (restore, and the version checks in stage_chunk). The shared code doesn't make Linux trivial, because Linux keeps a different store:

  • node_state::Store keeps a journal and a recorded interface key.
  • The private key lives in the kernel infraternal0 device, so rotating means deleting and recreating the interface (and its firewall adoption) on restore.
  • The Linux Installation context (uninstall::Context) would also need to carry the binding.

The macOS Enrolment type and the reconcile rule in macos_node.rs can be reused as the model.

**J80d (#265 round 2): macOS is done on the branch, Linux stays here.** macOS, `macos-node-265-s1` commit `fd959b443` ("bind Mac mesh state to the enrolment that issued it"): - `mesh-public.json` records a `binding`: sha256 of the issuing coordinator's CA certificate, the host id, and sha256 of the infraternal agent's mTLS public key. These are all public values from `identity.json`, built in `remote::run`. - On restore, state that another enrolment issued loses its config, routes and interface, and the WireGuard key file is removed. The node registers a new key. - Unbound state from slice-1 builds is adopted once. - `agent join` (root, macOS) removes `mesh-public.json` and the key after a successful enrolment. - The leaf certificate is deliberately not part of the binding, because it renews every 3.5 days. **Linux is not included.** The same rule exists in `node.rs` (`restore`, and the version checks in `stage_chunk`). The shared code doesn't make Linux trivial, because Linux keeps a different store: - `node_state::Store` keeps a journal and a recorded interface key. - The private key lives in the kernel `infraternal0` device, so rotating means deleting and recreating the interface (and its firewall adoption) on restore. - The Linux `Installation` context (`uninstall::Context`) would also need to carry the binding. The macOS `Enrolment` type and the `reconcile` rule in `macos_node.rs` can be reused as the model.
Sign in to join this conversation.
No labels
launch-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/infraternal#325
No description provided.