Tier 0: the questions answered, the decisions taken, and the design #9
@@ -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.
|
||||
Reference in New Issue
Block a user