Tier 0: the questions answered, the decisions taken, and the design #9

Merged
jschoubben merged 16 commits from design/close-the-record into main 2026-08-25 22:56:07 +00:00
2 changed files with 144 additions and 3 deletions
Showing only changes of commit 4866007c04 - Show all commits
@@ -1,5 +1,5 @@
---
status: proposed
status: accepted
date: 2026-08-25
deciders: jochen
reconstructed: false
@@ -89,8 +89,8 @@ makes it smaller.
- **The rarely-run path is now the common one.** The first node exercises the same code every
other node exercises constantly. That is the whole reason for choosing this over two modes.
- **The link becomes the security boundary.** 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. **Not decided here, and it is a gap.**
what may be pushed, and how a joining node proves it is entitled to join, is its own
question — taken up by [ADR 0039](0039-the-link-is-the-security-boundary.md).
- **The bundle must be able to raise the substrate alone.** Whether one host can bring up the
four pinned services with no mesh present is Move 1 of the skeleton and remains unproven.
This record depends on it and does not establish it.
@@ -0,0 +1,141 @@
---
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=<postgres password from novox>
REGISTRY_MINIO_PASSWORD=<minio password from novox>
```
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.