Files
hq/02-DECISIONS/0085-a-secret-is-a-provision.md
jschoubben baa3351552 Amend ADR 0085: the vault is a foundation module and holds the root secrets
Recorded on the record, dated, before anything shipped against the sentences
that change. The vault is installed at genesis like the store and broker, one
per mesh, and holds every secret a module has for itself sealed a second time
to an operator key whose private half never enters the mesh — the break-glass
path the first version left open, without a key one place holds.

Design 24 says how; 07 and 21 say what genesis does not yet do; issue 071
names the fixed credentials the foundation is raised with today.
2026-09-20 23:55:01 +02:00

154 lines
9.9 KiB
Markdown

---
topic: what runs on it
status: accepted
date: 2026-09-20
amended: 2026-09-20
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0031-the-control-plane-authenticates-nobody.md
---
# 85. A secret is a provision, and the vault is the module that provides it
## Context
[ADR 0031](0031-the-control-plane-authenticates-nobody.md) decided that identity *runs on the
mesh, not of it* — a module other modules require, rather than a privileged part of the
controller. [ADR 0078](0078-the-store-and-broker-are-modules.md) did the same for the store and
the broker: the twelve-module floor has no specialty left in it. **Secrets are the exception that
survived.** No module owns a secret.
Secret handling is smeared across three built-in parts of the runtime:
- the **controller mints** one credential per consumer↔provider pair and seals it to both node
keys ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md));
- the **mesh generates local secrets** — *"generated secrets are the mesh's, never authored"*
(as-is, `06-configuration-and-secrets.md`) — for a value a single module needs for its own use;
- the **synchroniser injects** both into a node's generated files.
Nothing is the owner of "a secret" the way the store module is the owner of "a database", and the
cost is recorded rather than hypothetical. The as-is design names the weakness in its own words:
*"Rotation is not a mesh operation… there is no mechanism that rotates one and informs everything
holding it."* A `rotate` command has since been built and proven, but it reaches **only** the
provisioned pairs; a secret a module generates for its own fully-local use — the password of a
version-pinned embedded store ([ADR 0084](0084-which-provider-serves-a-consumer.md)), an internal
token — is minted by the mesh and then has no operation that can remake it. Three species of
secret, and only the first has an owner:
| species | minted by | rotates? |
|---|---|---|
| a provisioned credential (a database login) | controller, sealed to nodes (0048) | yes — `rotate`, per pair |
| a module's own local secret | the mesh, as a generated value | **no owner, no rotation** |
| an operator-delivered secret (an external key) | a person, sealed in (`secret accept`) | no rotation, no audit |
## Considered Options
1. **Leave it a property of the controller.** Rejected: it is the smear above — no owner, local
secrets that cannot be rotated, operator secrets that cannot be audited — and it is exactly the
specialty 0078 removed for the store and broker, kept here for no reason anyone recorded.
2. **A dedicated vault built into the foundation, not a module.** Rejected: it reintroduces a
privileged built-in, the thing 0031 and 0078 went out of their way to remove, and a mesh that
wants none would still carry it.
3. **Fold all minting, the controller's provisioning credentials included, into the vault.**
Rejected: the controller must mint in order to **deliver** any provision — the vault's own
credential among them — so making the vault mint the credential of its own delivery is the
store/broker chicken-and-egg for no gain. The provisioned-pair credential already has an owner
and a rotation ([13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)); this record
does not disturb it.
4. **A secret is a provision; the vault is an ordinary module that provides it.** Adopted.
## Decision
**Secret-holding is a module, parallel to identity.** A module that needs a secret **for its own
use** — a local service's password, an internal token, an external key it was handed — requires a
`secret` provision from a vault provider, exactly as it requires a database from the store. The
vault generates the value (or holds one it was given), and because the credential belongs to the
consumer↔vault pair it **rotates, backs up and is audited through the same per-pair machinery**
[13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md) already defines. That is what
gives the second and third species the rotation and audit they lack. A module's own secret stops
being a generated value that nothing owns and becomes an ordinary provision with a provider.
**The controller's minting of provisioning-pair credentials is unchanged** ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)):
it is how every provision, the vault's own included, is delivered. The vault does not mint the
mesh's delivery credentials; it provides secrets to modules, and is itself provisioned the ordinary
way.
**The vault is node-scoped like every provider** ([ADR 0084](0084-which-provider-serves-a-consumer.md)):
each node its own, selected the same way, so a module's own secret is held by the vault on the
module's node. **A mesh that wants no vault runs none** — a module requiring no secret needs
nothing, which is the same test 0031 applied to identity.
## Consequences
The gap that opened this — a generated local secret with no rotation — **closes without new
machinery**: rotating such a secret is the vault's provisioner remaking a pair credential, the
operation 13 already specifies. Backup, audit and break-glass gain an owner — the vault module —
and become things a design specifies rather than absences. The as-is sentence *"rotation is not a
mesh operation"* is already false for provisioned pairs and, once the vault ships, for local
secrets too; the as-is document is updated when it does, not before.
What got harder: a break-glass path — recovering a secret when the sealed delivery path is
unavailable — must not reintroduce a key that one place holds, which is the property
[ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) was built to preserve; how
the vault offers recovery without it is left to the design as an open question. And a module that
today bakes a password into its own composition must instead require it from the vault — a
migration taken module by module, not a flag day.
## Amendment — 2026-09-20, before anything shipped
Recorded on the record itself rather than as a supersession, by its decider, on the day it was
accepted and before any code was merged against the sentences that change. The original text above
is left as written; this section says what it got wrong and what stands instead.
**What it got wrong.** The decision treated the vault as a provider like the store — optional, and
one per node — and left the mesh's own root secrets outside it: the store's superuser, the broker's
administrator, the controller's contexts, sealed to a node key and nothing else. Those are the
secrets with no rotation and no recovery, and they are the ones a vault exists for. At genesis they
are not even secret: the foundation raises its store and broker with fixed, well-known credentials
and carries those into the mesh. Leaving that floor in place gave module secrets an owner and the
root secrets none.
**What stands instead.**
- **The vault is a foundation module.** It is installed at genesis as part of the foundation
([ADR 0078](0078-the-store-and-broker-are-modules.md) is the precedent: a foundation piece is
still an ordinary module), not assigned later by a mesh that happens to want one. *"A mesh that
wants no vault runs none"* is withdrawn. A mesh has root secrets, so a mesh has a vault.
- **One per mesh, on the control-node.** *"The vault is node-scoped like every provider"* is
withdrawn. Node scoping ([ADR 0084](0084-which-provider-serves-a-consumer.md)) exists because a
store holds data a consumer is coupled to; the vault holds nothing a consumer is coupled to, and a
second one would be a second place to lose. Consumers on other nodes reach it as they reach
identity.
- **The vault holds the mesh's root secrets under an operator-held key.** The controller mints and
delivers exactly as before; in addition, every secret a module holds for itself is sealed a second
time, to an **operator sealing key** whose private half never enters the mesh. The vault keeps
those operator-sealed copies on its own disk, outside the store, and can hand them out — they are
ciphertext to everything but the operator. This is the break-glass path the original text left
open, and it does **not** reintroduce a key one place holds: the mesh holds blobs it cannot open,
and the operator holds a key with nothing to open until given a blob. Recovery needs both.
- **Genesis mints real root secrets and seals them to the operator key first**, so the fixed
credentials the foundation is raised with are replaced before the mesh is handed over.
**Unchanged.** A module's own secret is a `secret` provision the controller mints and the vault
records; the provisioned-pair path of [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)
is untouched; the vault stores no plaintext, ever.
**What it costs.** An operator key is a thing a person must keep, and a mesh whose operator key is
lost has root secrets that can be rotated but not recovered — the same standing as today, stated.
Sealing every own secret twice is a column and a call. Genesis grows a step. A module's
vault-provided secret (a pair credential) is not yet sealed to the operator key; that is the next
increment, not this one.
## References
- [ADR 0031](0031-the-control-plane-authenticates-nobody.md) — identity is a module; this is the
same move for secrets.
- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) — the provisioning-credential
path, left unchanged.
- [ADR 0078](0078-the-store-and-broker-are-modules.md) — the de-specialisation this completes.
- [ADR 0084](0084-which-provider-serves-a-consumer.md) — the node-scoping the vault obeys.
- [issue 068](../04-ISSUES/068-secrets-have-no-owning-module/00-report.md) — the gap.
- [`03-DESIGN/01-to-be/24-the-secrets-vault.md`](../03-DESIGN/01-to-be/24-the-secrets-vault.md) —
the design. The source mesh's `secret_locate` / `secret_backup` / `secret_verify` /
`secret_breakglass` subsystem is the prior art it draws on.