Files
hq/02-DECISIONS/0085-a-secret-is-a-provision.md
T
jschoubben fc4ab370d6 Graduate issues 067 and 068 to decisions and to-be designs
ADR 0084 (extends 0027) — a provision is served by a node-scoped provider the
consumer selects, defaulting to co-location; a module may instead carry a private
embedded instance that is not a provision. Design: 01-to-be/23-choosing-a-provider.

ADR 0085 (extends 0031) — a secret is a provision and the vault is the module that
provides it; a module's own local secret becomes an ordinary pair credential that
rotates through the existing machinery, while the controller's provisioning-credential
mint (0048) is unchanged. Design: 01-to-be/24-the-secrets-vault; doc 13 amended to
cross-link the non-pair secret.

Issues 067/068 marked resolved with amended-design set. ADR index regenerated;
records and index checks pass.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-20 21:27:29 +02:00

108 lines
6.6 KiB
Markdown

---
topic: what runs on it
status: accepted
date: 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.
## 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.