Files
hq/03-DESIGN/01-to-be/24-the-secrets-vault.md
T

150 lines
9.9 KiB
Markdown

---
layer: to-be
status: implemented
code: [mesh-catalog, mesh-controller, mesh-host]
updated: 2026-09-21
decisions:
- 02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md
- 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md
- 02-DECISIONS/0085-a-secret-is-a-provision.md
- 02-DECISIONS/0031-the-control-plane-authenticates-nobody.md
- 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
---
# 24 — The secrets vault
Identity runs on the mesh, not of it — a module other modules require, not a part of the
controller ([ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md)). The
store and the broker are ordinary modules too
([ADR 0078](../../02-DECISIONS/0078-the-store-and-broker-are-modules.md)). Secrets are the piece
that never got the same treatment: nothing owns a secret. This document describes the module that
does — a **vault** that provides a `secret` provision — and, as importantly, the boundary of what
it owns and what it deliberately does not.
## Three kinds of secret, and which the vault owns
A mesh handles three species of secret, and confusing them is how the current arrangement went
wrong.
- **A provisioned credential** is the login one module uses against another — a database password,
a broker account. The mesh mints it per consumer↔provider pair and seals it to the machines that
must hold it ([ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md)),
and its provider makes it true. **This is not the vault's**, and the vault does not change it. It
already has an owner and a rotation
([13](13-credentials-and-their-rotation.md)); disturbing it would buy nothing.
- **A module's own secret** is a value a single module needs for itself — the password of a store
it runs privately, an internal signing token. Today the mesh generates this as a value nothing
owns, and so it cannot be rotated. **This is the vault's.**
- **An operator-delivered secret** is a value only a person can supply — a credential for something
outside the mesh. Today it is sealed in and then held, un-audited and un-rotatable. **The vault
holds this**, and can hand it out and audit it, though it cannot generate it.
The vault, then, is the provider a module turns to for a secret that is **not** the byproduct of
some other provision. It is the answer to *"this module needs a password, and there is no provider
whose job it is to give it one."*
## A secret as a provision
A module that needs a secret for its own use requires a `secret` provision, exactly as it requires
a database from the store. The vault generates the value — or takes custody of one an operator
delivered, through `secret accept … --provider`, which seals it to both ends and records the pair
as accepted ([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)) — and
the credential belongs to the consumer↔vault pair. A module that needs several values names them
as local names under its `secrets` entry, and each is a pair of its own — keyed on the local
name, delivered as its own file, held at the vault as its own holder
([ADR 0094](../../02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md)).
*How it is checked:* manifest, resolver and inventory tests on two local names; the vault bed's
two-secret consumer. An accepted pair is the one exception to what follows: the mesh cannot make its replacement, so it is neither remade when a key changes nor
rotated; both are refused aloud, and accepting a new value is the rotation. *How it is checked:*
an inventory test accepts, reads back unchanged, and asserts the two refusals name the remedy. Because it is an ordinary pair
credential, **everything already built for pair credentials applies to it unchanged**: it rotates
with the one command that discards a credential and delivers both ends together, it is one secret
per holder so rotating one touches nothing else, and *who holds this* is a query rather than an
assumption ([13](13-credentials-and-their-rotation.md)). The rotation a module's own secret lacks
today is not new machinery; it is the machinery that already moves a database password, pointed at
a secret the vault provides.
This is what replaces the "generated value that nothing owns". A module's own password stops being
a special kind of thing injected by the synchroniser and becomes a provision with a provider, a
holder, and a lifecycle — the same shape as everything else the mesh grants.
## The vault is a foundation module, one per mesh
The vault is an ordinary module — built, assigned and upgraded like any other — and it is part of
the foundation: installed at genesis, the way the store and broker are raised first and then
adopted as modules ([ADR 0078](../../02-DECISIONS/0078-the-store-and-broker-are-modules.md)). A
mesh has root secrets, so a mesh has a vault; it is not something a mesh opts into
([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), amended). It is provisioned the
ordinary way, and it does **not** mint the mesh's delivery credentials — the controller does that,
because the controller must mint in order to deliver any provision, the vault's own included.
There is **one vault per mesh**, on the control-node, reached from any node the way identity is.
Node scoping ([ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md),
[23](23-choosing-a-provider.md)) exists because a store holds data a consumer is coupled to; a
vault holds nothing a consumer is coupled to, and a second one would be a second place to lose.
## The root secrets, and the operator key
The mesh has secrets of its own that no module requires from anybody: the store's superuser, the
broker's administrator, the controller's credentials to its contexts, the sealing key of every
node. Until now each was sealed to the node that uses it and to nothing else, so a node whose key
was gone took them with it — and at genesis they were not even secret, the foundation being raised
with fixed credentials that were then carried in.
The vault's second job is to hold these, and it does so without holding a value:
- **An operator sealing key.** A keypair made once, by the operator, whose private half is written
to a file the operator keeps off the mesh and whose public half the mesh records. It is made
before anything else is minted, so that everything is sealed to it.
- **A second seal.** Every secret a module holds for itself — minted or accepted — is sealed to its
node as before and, in addition, to the operator key. What the mesh stores is one more blob it
cannot open. A secret made before the key existed has no such copy and cannot get one, the
plaintext being gone; the mesh says which those are rather than letting the export pass for
complete.
- **The export.** All copies sealed to the mesh's current operator key, the key's public half and
its fingerprint, and two honest lists beside them: what is sealed to an earlier key the mesh has
since replaced, openable with that key alone, and what has no operator copy at all. As one
document. The vault declares that it *keeps* this, and
the mesh writes it onto the vault's own disk as an ordinary declared file — ciphertext to the
machine that holds it and to the bus it crossed. The same document can be written out by the
operator to keep beside the key.
- **Recovery.** The operator, holding the private key, opens one secret from the export or from the
store and gets it as a file, never on a terminal unless asked for. It needs the key and a blob;
the mesh has the blobs and no key, the operator the key and no blob until given one. Nothing in
this reintroduces a place that can open everything, which is the property
[ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md) was
built to keep — so the break-glass question the first version of this design left open is
answered by it.
**Genesis** makes the operator key first and mints real root secrets in place of the fixed ones the
foundation is raised with, so the mesh is handed over with nothing well-known in it. The installer
does this ([21](21-the-installation-in-full.md)); what it runs is described in the as-is
([`00-as-is/06`](../00-as-is/06-configuration-and-secrets.md)).
A module's vault-provided secret — the pair credential of
[13](13-credentials-and-their-rotation.md) — is sealed to the operator the same way, as is every
credential a provider grants; the export names each entry by the node and module that hold it and
the name they know it by, and says whether it is a module's own secret or a pair credential, so
recovery addresses both alike.
## Beyond generate and hold
Owning a secret means owning more than its creation. The mesh being migrated onto has a working
secrets subsystem whose surface names the operations a vault is responsible for — locating a secret,
backing it up, verifying it is what it should be, and a break-glass recovery for when the normal
path is unavailable. These become the vault module's, specified against it rather than scattered.
Break-glass is answered above, and by the property it had to keep: **it does not reintroduce a
key that one place holds.** The vault keeps blobs sealed to the operator; the operator keeps a key
with nothing to open until handed a blob. Locating and verifying are the vault's tools, by
fingerprint; backing up is the export.
## What this changes for a module
A module that today writes a password into its own composition — an embedded store's login, an
internal token — instead requires it from the vault and reads it where the mesh puts it. The change
is taken module by module, not as a flag day, and it is the same change in each: a value the module
authored becomes a value the vault provides. When it is done, the guarantee the as-is design already
makes for generated secrets — *nothing in the repository contains a credential* — holds for a
module's own secrets not by convention but because there is a provider whose job it is to keep them.