--- status: proposed date: 2026-08-27 deciders: jochen reconstructed: false extends: 0039-the-link-is-the-security-boundary.md --- # 51. The enrolment token carries where the mesh is and how to recognise it ## Context [ADR 0039](0039-the-link-is-the-security-boundary.md) requires **mutual** authority: the node proves it may join, and **the control plane proves it is the mesh**. It states why one-way is not enough — the host applies whatever the link delivers, so *an attacker who can answer a joining node's first call owns the machine.* It does not say **how** the control plane proves itself, and [ADR 0049](0049-a-route-is-a-grant.md) deferred the question again, noting only that internal identity and public exposure are two different certificate stories. Left unanswered it produces a circle. Verifying the mesh needs the mesh's CA. Obtaining the CA means trusting whatever hands it over — which is the thing being verified. There is a second circle in the same place, and today they are solved by the same unfortunate mechanism. A node must reach the mesh before the mesh has configured it, so it cannot yet resolve any mesh name. [Research 004](../01-RESEARCH/004-lab-network/analysis.md) records the workaround: > `dnsmasq-app` generates `.internal` names on each node and writes an `/etc/hosts` block **as a > floor underneath, because a node must reach the mesh DB before its own DNS exists.** and, separately, that a joining node must have *"registry database and object-store host and credentials, plus an npm token"* placed on disk beforehand — the credentials [ADR 0039](0039-the-link-is-the-security-boundary.md) exists to remove. **Both circles are the same shape: a node needs some fact about the mesh before it has any trustworthy way to obtain one.** Whatever supplies that fact must arrive by a path other than the mesh. ## Considered options 1. **Ship the CA with the host binary.** Then the binary is mesh-specific, which [ADR 0041](0041-the-host-depends-on-nothing.md) and [ADR 0046](0046-the-installer-fetches-what-it-pins.md) both work to avoid, and rotating the CA means rebuilding and redistributing the host everywhere. Rejected. 2. **Trust on first use, plainly.** Accept whatever answers the first call and pin it. Rejected: it is exactly the attack ADR 0039 names, and the first call is the one moment the node has no way to tell. 3. **A public certificate authority for the control plane's own endpoint.** Workable, and it makes joining depend on public DNS and public issuance for a link that is otherwise entirely the mesh's business. Rejected as a dependency, not as a technique — a mesh whose nodes cannot join because an unrelated public authority is having a bad day has bought nothing. 4. **The token carries it.** Chosen. ## Decision **The enrolment token carries three things**, and it is the only thing a joining node needs: | | | | |---|---|---| | **where** | an **address**, not a name | there is no resolution yet, and this is why none is needed | | **who** | the fingerprint of the identity to expect | what makes the mesh provable rather than assumed | | **the right to join** | the one-time secret ADR 0039 already specifies | useless once used, useless after it expires | The token is issued by the mesh for one enrolment and **carried out of band** — by the person adopting the machine. That is what breaks both circles: its authenticity comes from the channel it travelled, not from anything the node can check afterwards. **This is trust-on-first-use with the first use moved out of band**, which is the difference between a pin and a guess. The node does not accept whatever answers; it accepts the one thing it was told to expect, before it spoke to anything. ### What this settles **The mesh CA is not a bootstrap concern.** It is how `.internal` names are certified once a node is a member, and nothing needs it earlier. The open item [ADR 0049](0049-a-route-is-a-grant.md) left — *what the control plane presents to a node that trusts nothing yet* — is closed: it presents the identity whose fingerprint the token named. **Nothing needs name resolution before the link exists**, because the token carries an address. The `/etc/hosts` floor exists to solve a problem that stops existing, and it should go rather than be carried forward — a fallback nothing needs is a path nothing tests. **Nothing is placed on disk beforehand except the token.** No database credential, no object-store credential, no registry token. That is ADR 0039's central claim finally made true at the one moment it was still false, and it is the difference between *a node holds only its own identity* being a design statement and being a fact. ## Consequences - **The token becomes security-critical in a way it was not**, because it now carries the pin. Tampering with it in transit substitutes the mesh. That is a real exposure and it is strictly better than the alternative: without a pin there is nothing to tamper *with*, and the node trusts the first answer unconditionally. The exposure moves to a channel a person controls and can verify, from one nobody could. - **Token delivery is now a designed step, not an incidental one.** It is short-lived and single-use, so interception is bounded — but how it reaches a machine is part of adoption and needs saying. [Research 012](../01-RESEARCH/012-the-minimum-viable-node/00-overview.md) is where that belongs. - **Rotating the control plane's identity invalidates outstanding tokens**, which is correct and needs to fail legibly. A node presenting a token with a stale fingerprint must be told that, not left to time out. - **The address in the token can go stale.** If the control plane moves, unissued tokens point somewhere wrong. Tokens are short-lived, which bounds it; moving the control plane is [`06`](../03-DESIGN/01-to-be/06-the-control-plane.md)'s undesigned territory regardless. - **A rejoining node is an ordinary case, not a special one.** A node that has lost its identity gets a new token. There is no recovery path to design because there is no long-lived secret to recover. ## References - [ADR 0039](0039-the-link-is-the-security-boundary.md) — the mutual authority this implements. - [ADR 0038](0038-a-node-joins-by-linking-first.md) — the join this is the first step of. - [ADR 0049](0049-a-route-is-a-grant.md) — the open item this closes. - [Research 004](../01-RESEARCH/004-lab-network/analysis.md) — the `/etc/hosts` floor and the credentials placed beforehand.