ADR 0113 and to-be 27: the vault makes every secret — provisioning all the way down

A secret comes into being seven ways today: provider credentials, own secrets (54 modules), broker
accounts through a command that is easy to forget, a vault that only records what the controller
mints (6 modules), operator values, licences, and root secrets. The vault was built to end own secrets
and did not; the old path was never retired.

0113 is rewritten as a waterfall. The vault makes every secret and nothing else does. A provider that
needs a secret for a consumer requires it from the vault, declared once in its provision's contract
and expanded per consumer by resolution; the vault delivers it to both holders, each sealed to its own
node, so a provider's code is unchanged. Own secrets, broker passwords, operator values and licence
credentials take the same path. Genesis is not an exception: it raises the vault first and asks it,
so there is one way a secret is made from the first one on. The vault can sit at the bottom because it
requires nothing but a broker account.

One shared mint function in the SDK was considered and rejected: generation becomes uniform but custody
stays spread over every provider's machine, and each SDK language needs its own implementation.

Rotation is asked of the vault and is provider-first: the value goes to the holder that accepts it,
which confirms, before the holder that presents it gets it, so the lockout window shrinks to the
consumer's own restart, and an unconfirmed provider holds the rotation rather than half-doing it. The
host derives which processes to restart or recreate from the requirement a definition reads, so no
definition declares restart-on for a secret. A rotation shows unconfirmed until each consumer restarted
and passed its health check. Issue 103 becomes a prerequisite.

The file is renamed to match what it now decides. 0112 follows.
This commit is contained in:
jochen
2026-09-25 23:10:52 +02:00
parent 6e3373c879
commit fa2c09a2c5
6 changed files with 265 additions and 173 deletions
@@ -5,7 +5,7 @@ code: []
updated: 2026-09-25
decisions:
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
- 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
@@ -69,11 +69,29 @@ Which module answers, in order:
5. otherwise **refused**: naming the unheld seat, for a provision a seat delivers, even when exactly
one provider exists; naming the candidates otherwise.
The provider makes what it provides and answers with its contract's fields. The mesh carries the
answer back to the consumer, sealing every secret field to the consumer's node
([ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)). The vault
is a module provider like any other: it holds the `mesh-vault` seat and generates the secrets it
provides.
### Secrets: provisioning all the way down
**The vault makes every secret, and it is the only thing that does**
([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)). It holds the `mesh-vault` seat,
so every `secret` requirement resolves to it.
**A provider that needs a secret for a consumer requires one, like any consumer.** A provision's
contract declares it: *for each consumer, one secret*. Resolution expands that into one requirement
per consumer, named for that consumer:
1. gitea requires `postgres-database`;
2. the database provider, to serve gitea, requires a `secret` named for gitea;
3. the vault makes it and hands it to the mesh;
4. the mesh delivers it to both holders, each sealed to its own node: the database's machine, to
create the login, and gitea's, to present it;
5. the database provider creates the login, exactly as it does today, and gitea connects.
A module's own secret, a broker account's password and a secret operator value take the same path.
Nothing in the mesh makes a secret except the vault.
**A provider makes resources and data.** Beyond secrets, a provider answers with its contract's
non-secret fields: an analytics site id, a registered public name. The mesh carries them back to the
consumer as resolved values.
### The node's host
@@ -116,11 +134,11 @@ A value a person chooses: a public name for an endpoint, a greeting, how many wo
needs no provider module, no grant and no credential. If asking a person for a value took more than
that, module authors would route around it, and the literals this replaces would come back.
**A secret operator value**, such as an external API key, is still an operator requirement: its
provider is the operator. What differs is where it is kept. The vault holds it as an
operator-delivered value ([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)),
never as a setting, because anything secret belongs in one place that can seal and audit it. The
vault is its custodian, not its provider.
**A secret operator value**, such as an external API key, follows the one rule for secrets: the
vault provides it. The operator hands the value to the vault, once
([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)), and the module requires
a `secret` like any other. The only difference is that rotation never replaces it: the vault cannot
make a new external key, so rotating one means an operator handing over a new value.
**An endpoint** is an operator value inside a route requirement: the public name is chosen on the
assignment, and the route provider answers. A public name already held by another assignment is
@@ -162,10 +180,36 @@ slug, under the same rules as a slug. This has to be settled before a second ins
## Genesis
The one exception. Before any provider exists, genesis answers the foundation's own requirements
itself: the store's and broker's credentials, the vault's own access, and the root secrets. It seals
them to the operator key as it does now ([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)).
After genesis, nothing is answered except by a provider.
**Genesis is the vault's first answer, not an exception.** It raises the vault before anything else
and asks it for the foundation's secrets: the store's superuser, the broker's admin in the hashed form
the broker needs, and the vault's own broker account. The vault answers with the same code it always
uses, before the bus exists, and seals the root secrets to the operator key as today
([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)). Genesis makes no secret itself, so
there is one way a secret is made, from the first one onwards.
The vault can sit at the bottom because it requires nothing but a broker account: it keeps its data
on its own disk, not in the store. So the waterfall ends at the vault.
## Rotation
Rotating a secret is asked of the vault, by an operator or by the vault's own policy, such as a
maximum age in the secret's contract ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)).
1. **The vault makes the new value.**
2. **It is delivered to the holders that accept it first.** For a database credential that is the
database's provider, whose provisioner applies it and confirms it did.
3. **Only then is it released to the holders that present it**, such as gitea. A consumer is never
sent a value its provider has not accepted, so the window in which it cannot log in shrinks to its
own restart. A provider that does not confirm holds the rotation: the consumer keeps the old value,
which still works, and the rotation shows as waiting on that provider.
4. **The host restarts every process that reads the secret**, and recreates a container whose
env-file carries it. It knows which, because a definition reads a secret only through its
requirement. No definition declares a restart for a secret. This needs
[issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)
fixed, or a container fed by an env-file keeps the old value.
5. **It is confirmed.** The rotation shows as unconfirmed until each consumer has restarted with the
new value and passed its health check, where its definition declares one. Delivered and working
are shown as different things.
## Refusing
@@ -175,10 +219,10 @@ requirement it names what is missing and what would answer it:
- an unheld seat, and which modules could hold it;
- no provider, and which modules could provide it;
- an operator value with no default, and that the assignment must give it;
- a provider that has not answered yet, and which one.
- a provider, or the vault, that has not answered yet, and which one.
The last one is a state, not a failure. A consumer waiting for its provider is shown as waiting, and
nothing is delivered until the answer arrives ([ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)).
The last one is a state, not a failure. A consumer waiting for its provider or for the vault is shown
as waiting, and nothing is delivered until the answer arrives.
## What this retires
@@ -188,7 +232,9 @@ nothing is delivered until the answer arrives ([ADR 0113](../../02-DECISIONS/011
| settings on an assignment ([ADR 0046](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md)) | operator requirements, addressed to an instance |
| a port the mesh assigns | a host requirement |
| machine facts and machine placeholders | host requirements |
| a secret the controller mints | a secret the vault makes ([ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md)) |
| every secret the controller mints: provider credentials, own secrets, broker passwords, root secrets | a secret the vault makes ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)) |
| a separate command issuing a broker account | a requirement resolved on assignment |
| `restart-on` naming a secret's file | a restart the host derives |
| paths in resources, mounts, bindings, secrets and received files | host directory requirements, placed by the assignment |
| literals carried in a definition | operator requirements with defaults |
@@ -204,10 +250,12 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
the controller. The controller resolves requirements from the four providers, refuses as above,
and fills the one form. Old mechanisms keep working beside it. *Ends when* a definition written
entirely in the new form installs on a lab machine.
2. **Providers answer.** The SDK harness answers with contract fields and seals the secret ones, the
controller carries answers back, and the vault holds its seat and generates. *Ends when* the
analytics and DNS providers answer their consumers, and a module's own secret is generated by the
vault and rotated by it.
2. **The vault makes every secret, and providers answer.** The vault holds its seat and is the only
maker; resolution expands per-consumer secret requirements; genesis asks the vault first; the mesh
carries providers' data back. [Issue 103](../../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)
is fixed first. *Ends when* no code path outside the vault makes a secret, the analytics and DNS
providers answer their consumers, and a database credential rotates provider-first, with the
consumer restarted by derivation and the rotation confirmed.
3. **Definitions move.** Every catalogue definition is rewritten, adopted and running assignments
placed where their data already is. *Ends when* the list of definitions using an old form is
empty, and the old forms are removed.
@@ -229,6 +277,9 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
| A public name already held is refused | A resolution test: a second instance asking for a public name the first holds is refused, naming the first. |
| Everything keyed by a module is keyed by its instance | A resolution test: two instances of one module on one node get two directories, two containers, two logins and separate settings. |
| A consumer waits for its provider | A resolution test with a provider that has not answered: shown as waiting, and nothing delivered. |
| Only the vault makes secrets | A controller test: no code path mints a secret, genesis included. |
| A provider's per-consumer secret comes from the vault | A resolution test: requiring a database expands to a secret requirement named for the consumer, answered by the vault and delivered to both holders. |
| Rotation is provider-first, derived and confirmed | The rotation tests of [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md): the consumer waits for the provider's confirmation, is restarted without a declared restart, and shows unconfirmed until healthy. |
| Refusal names everything at once | A resolution test with three unresolved requirements of different kinds: one refusal naming all three. |
| The old forms retire | The catalogue test listing definitions still using one. It must be empty before a form is removed. |
+1 -1
View File
@@ -35,7 +35,7 @@ document is written and this one's status becomes `implemented`.
| [`23-choosing-a-provider.md`](23-choosing-a-provider.md) | Which of several providers of a kind serves a consumer, and when a module carries its own instead | [ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), [ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) |
| [`24-the-secrets-vault.md`](24-the-secrets-vault.md) | The module that owns a secret — a `secret` provision, and the boundary of what it owns | [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), [ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md), [ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md) |
| [`26-the-seats.md`](26-the-seats.md) | What a mesh can have one of, who fills each, and a seat's holder answering for the provision it delivers — including the `git` seat a build's source can live on | [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md), [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md), [ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md) |
| [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-a-provider-makes-what-it-provides-and-the-mesh-carries-it.md), [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md) |
| [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md) |
## Not yet written