150 lines
9.9 KiB
Markdown
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.
|