--- status: accepted date: 2026-08-25 deciders: jochen reconstructed: false extends: 0038-a-node-joins-by-linking-first.md --- # 39. The link is the security boundary ## Context [ADR 0038](0038-a-node-joins-by-linking-first.md) makes the link the one channel a node takes declarations from, and names the gap it leaves: *"everything a node applies arrives through it, so what may be pushed, and how a joining node proves it is entitled to join, is now a question worth its own record."* This is that record. It is a design decision about a boundary that does not exist yet — but what it replaces is measured, and that is the argument. Settled as: **a node owns no password. It owns an identity, and that identity is what it presents to the broker.** ### What adoption does today `install.d/adopt.sh` asks the operator to paste credentials in by hand: ``` The meshware module needs registry database and minio credentials. REGISTRY_DB_PASSWORD= REGISTRY_MINIO_PASSWORD= ``` plus an `NPM_TOKEN` for the private registry. These are not adoption-time credentials that are then discarded: `wireguard` and `traefik` open a `pg` connection on every reconcile ([ADR 0037](0037-the-host-applies-it-does-not-decide.md)). **So every node permanently holds a credential to the control plane's database, and to the object store.** They are the same credentials on every node. There is no rotation — [`00-as-is/06`](../03-DESIGN/00-as-is/06-configuration-and-secrets.md) records that *"there is no mechanism that rotates one and informs everything holding it. Where a rotation has been done, it has been done by hand, and doing it wrong has taken services down."* Compromise of any node is therefore compromise of the mesh's database, and there is no mechanism to recover from it. ### The link is not new Written first as though the link were a thing to build. It is not. [ADR 0001](0001-nodes-communicate-over-a-broker.md) already has it: *every node connects outbound to a single broker; nothing ever connects to a node*, each node declaring an exchange named for itself and consuming from its own queue ([`00-as-is/01`](../03-DESIGN/00-as-is/01-mesh-and-transport.md)). That is already outbound-only, already per-node addressed, and already the one channel everything arrives through. **This record is not proposing a channel. It is proposing that the channel carry per-node identity instead of one shared credential.** The same as-is records the fault, for the broker rather than the database: *"the broker is a single point of failure and a single point of trust. Its credential is mesh-wide, so rotating it is a mesh-wide operation, and doing it wrong has taken the broker down."* ## Considered options 1. **Keep shared credentials, scope them per node.** Least change: give each node its own database role. Rejected — it makes the blast radius smaller without changing its shape, and it keeps tier 0 speaking the control plane's schema, which ADR 0037 forbids for reasons that are not about security at all. 2. **Accept the exposure as the cost of simplicity.** A shared credential is one thing to understand and nothing to build, and the objection to replacing it is real: mutual authentication fails opaquely, and a node that cannot link is harder to debug than a node with a wrong password. Rejected on the ground that the simplicity is what makes it unrotatable — the credential cannot be changed *because* everything holds the same one, so the arrangement's convenience and its unfixability are the same property. 3. **Mutual authority on a node-initiated link, with the node holding nothing but its own identity.** Chosen. ## Decision **The link is the only way anything reaches a node**, and four properties make it a boundary rather than a pipe. ### It is outbound and node-initiated The node dials the control plane. Nothing dials a node. This is not only defensive — it is what the topology already requires: most nodes sit behind a household connection with no forwarded port ([research 004](../01-RESEARCH/004-lab-network/00-overview.md)), so an inbound control channel would work for the hosted node and not for the rest, and the difference would be invisible until it mattered. A node therefore has **no listening control surface at all**. ### A node holds its own identity and nothing else No shared secret, no credential to anything it does not own. A node's identity authenticates it to the control plane and grants access to nothing else. **Compromise of a node is compromise of that node.** That is the property today's arrangement does not have, and it is the main reason for this record. ### Authority is mutual The node proves it may join, and **the control plane proves it is the mesh**. One-way is not enough here: the host applies whatever the link delivers, so a node that cannot tell the mesh from something impersonating it will apply that something's declarations. Given ADR 0038, an attacker who can answer a joining node's first call owns the machine. ### What may be pushed is bounded by form, not by trust The control plane may push **declarations of known shape** and nothing else. It may not push a command to run. The host's vocabulary is finite, versioned and auditable, and anything outside it is refused rather than best-effort interpreted. **Stated honestly: this bounds form, not impact.** A compromised control plane can declare harmful state — a malicious package, an open firewall — and the host will apply it faithfully, because that is what it is for. What the property buys is that the blast radius is describable: it is exactly what the declaration language can express, which can be reviewed. An arbitrary command channel has no such bound. This is a real limit and not a defence-in-depth story. ### Joining is a deliberate, bounded act A joining node presents a **one-time, short-lived enrolment token** issued by the mesh for that purpose, and exchanges it for its own durable identity. The token grants exactly one thing: the right to become a node. It is not a credential to any service, it does not persist after exchange, and it expires whether used or not. This replaces hand-carried shared secrets with a thing that is useless once used and useless after a while. ## What this actually costs The objection to weigh is overhead, and it is smaller than it looks because most of it is already running. | Property | Where it comes from | |---|---| | outbound, node-initiated | already true — ADR 0001 | | per-node addressing | already true — per-node exchange and queue | | per-node credential | a broker user per node; the broker already has users, virtual hosts and per-queue permissions | | mutual authority | transport-level certificates on a connection that already exists | | bounded by form | already true — three message shapes and only three | | **enrolment** | **the one genuinely new mechanism** | And ADR 0037 subtracts rather than adds: under it a node holds **no** database credential at all, so this record replaces three hand-carried shared secrets with one per-node identity that grants only identity. **It must fail legibly.** A boundary that refuses a node without saying why is worse than the credential it replaced, because a wrong password at least announces itself. A node that cannot link must report which side rejected it and on what grounds, in terms someone can act on. This is `how-we-build` §5 applied to a security mechanism: a refusal that proves only that something went wrong is transport reported as effect. ## Consequences - **ADR 0037 removes a standing exposure as a side effect.** Its rule — the host never queries the mesh database — was chosen for tier discipline. It also removes the reason every node holds the database password. Worth recording because the two arguments are independent and both hold. - **Rotation becomes possible and is still not designed.** Per-node identities can be revoked individually, which is what makes rotation tractable at all. The mechanism — what rotates, on what trigger, and how holders learn — is **not decided here** and remains the open weakness `00-as-is/06` records. - **The enrolment token has to come from somewhere.** Issuing it is a control-plane operation and the first node has no control plane, so the first node's identity is self-issued and becomes the root of trust when the mesh comes up. **That is a real asymmetry** — the one place ADR 0038's "no special first node" does not fully hold — and it is named here rather than hidden. - **A declaration vocabulary is now a security artefact, not only a design one.** Every addition widens what a compromised control plane can express. That is a reason to keep it small and a reason for additions to be reviewed as such. - **Offline nodes need identities that survive disconnection.** Per [ADR 0036](0036-a-node-is-a-managed-machine.md) disconnection is ordinary, so an identity that must be refreshed to remain valid would make a laptop fail for being a laptop. What expires and what does not is **not decided here**. - **This is a boundary that does not exist yet.** Nothing in the current mesh implements any of it, and the migration from shared credentials to per-node identity touches every node and the substrate. No estimate is offered. ## References - [ADR 0038](0038-a-node-joins-by-linking-first.md) — the link, and the gap this fills. - [ADR 0037](0037-the-host-applies-it-does-not-decide.md) — why the host stops holding database credentials at all. - [ADR 0036](0036-a-node-is-a-managed-machine.md) — disconnection as ordinary, which constrains what may expire. - [`00-as-is/06-configuration-and-secrets.md`](../03-DESIGN/00-as-is/06-configuration-and-secrets.md) — secrets today, and the absence of rotation. - [Research 004](../01-RESEARCH/004-lab-network/00-overview.md) — why most nodes cannot accept an inbound connection.