0003 is now superseded by 0056. Nothing is left proposed. Applied: - 06 corrected from ten contexts to seven plus the api, each row now stating why it passes the more-than-one-node test. work, knowledge and stream are named as mesh-hosted rather than dropped; `ai` folds into config; `record` is deferred explicitly rather than listed. Its frontmatter now cites 0055. - how-we-build §4 amended per 0054, and the derived page republished by playbook 05. The sync found the drift the playbook exists to catch: the published §4 and the source did not say the same thing. The source said "four accidents, not four boundaries"; the published page said "one intent expressed four times", and only the published page carried the scope caveat. Same rule, two texts, already diverging. Verified the republish by reading back -- the new rule is present and the old section's body returns nothing -- rather than trusting the success message. The two smaller findings: - 0051 separated the transport identity from the declaring authority. It said the token carries "an address" and "the identity to expect" without saying what the node dials. It dials the broker, so pinning only that would make the control plane's authority transitive and let a compromised broker forge declarations -- which, since the host applies whatever the link delivers, is the whole machine. The token now carries four things, and declarations are signed and verified per declaration. Cost recorded: rotating the signing identity is fleet-wide. - 0026 no longer restates 0022's rule about generated views. 0022's own words are "prose does not restate status; one place, and two is one too many", which is what 0026 was doing to it.
146 lines
8.4 KiB
Markdown
146 lines
8.4 KiB
Markdown
---
|
|
status: accepted
|
|
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 four things**, and it is the only thing a joining node needs:
|
|
|
|
| | | |
|
|
|---|---|---|
|
|
| **where** | the **broker's address**, not a name | a node dials the broker ([ADR 0001](0001-nodes-communicate-over-a-broker.md)); there is no resolution yet, and this is why none is needed |
|
|
| **what it is connecting to** | the fingerprint of the **broker's** certificate | so the node reaches the mesh's bus and not something answering in its place |
|
|
| **who it will believe** | the **control plane's** signing identity | 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 endpoint and the authority are two identities, not one
|
|
|
|
This is worth separating because collapsing it is the easy mistake, and the collapsed version
|
|
silently fails to deliver what [ADR 0039](0039-the-link-is-the-security-boundary.md) asks for.
|
|
|
|
A node connects to the **broker** and takes instruction from the **control plane**, which sits
|
|
behind it. Pinning only the broker would make the control plane's authority *transitive* — the
|
|
node would believe a declaration because of where it arrived from. **A compromised broker could
|
|
then forge declarations**, and since the host applies whatever the link delivers, that is the
|
|
whole machine.
|
|
|
|
So the node verifies **the transport** and **each declaration** separately:
|
|
|
|
- the broker, by its certificate, at connect time;
|
|
- the control plane, by a **signature on the declaration itself**, every time.
|
|
|
|
Then 0039's *the control plane proves it is the mesh* holds against a hostile broker rather than
|
|
assuming a friendly one — which matters because the broker is the one component every node must
|
|
reach and the one most exposed.
|
|
|
|
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.
|
|
- **Declarations must be signed, and that is a real requirement rather than a note.** The host
|
|
verifies a signature before applying anything, which adds a key to what it must carry and a
|
|
failure mode it must report legibly — *this declaration is not from the mesh I joined* is a
|
|
different condition from *this declaration is malformed*, and they must not read alike.
|
|
- **Rotating the control plane's signing identity is a fleet-wide operation**, because every node
|
|
holds the previous one. That is the cost of not trusting the broker, it is accepted, and it
|
|
needs a rollover that overlaps rather than a flag day.
|
|
- **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.
|