--- status: proposed 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, so it is proposed rather than measured — but what it replaces is measured, and that is the argument. ### 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. ## 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. **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. ## 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.