The fact-check found mailu, whose user is its mailbox, so 0114 rotates over two credentials rather than two logins, the adapter choosing what a credential is. Also: minio keeps non-empty buckets; five backends take their admin credential only at first init, so single-party rotation is staged; postgres ownership moves to a non-login role; the harness keys by consumer; rotation state lives with the vault. Consistency fixes across 0110-0113, 26 and 27; issue 103 resolved by mesh-host PR #22.
22 KiB
topic, status, date, deciders, reconstructed
| topic | status | date | deciders | reconstructed |
|---|---|---|---|---|
| what runs on it | proposed | 2026-09-25 | jochen | false |
113. The vault makes every shared secret, a provider makes resources and data, and the mesh carries both
Context
A shared secret comes into being many different ways today, counted across the catalogue and the controller on 2026-09-25:
| kind | made by | used by |
|---|---|---|
| a credential between a consumer and a provider | the controller | 16 modules, and 3 more for model access, counted below |
a module's own secret (own-secrets) |
the controller, as a random value nothing owns | 54 modules |
| a module's broker account | the controller, but only when a person runs a separate command; otherwise the random value above, which cannot work (issue 095) | 49 modules |
| a node's and the builder's broker accounts | the controller, each in its own code path | every node, the builder |
| an enrolment token | the controller | every node joining |
a secret from the vault |
the controller mints it, and the vault only records it (ADR 0085, as amended) | 6 modules |
| a value an operator accepts | a person (ADR 0092) | where accepted |
| a licence for model access | a separate controller context with its own store | model consumers |
| the foundation's root secrets | genesis, sealed to the operator key | the foundation |
The vault was built to end the second row, and did not. ADR 0085 says a module's own secret "stops being a generated value that nothing owns". 54 modules still use one, and 6 use the vault. The replacement was added and the old path was never retired.
ADR 0085 considered and rejected making the vault the only maker, because "the controller must mint in order to deliver any provision — the vault's own credential among them": the vault cannot make the credentials that exist before it does. That objection is real, and this record has to answer it rather than step around it.
Rotation has gaps. To-be 13 makes rotation one command, all-or-nothing, with a stated window in which a consumer cannot authenticate. A consumer restarts only if its definition remembered to say so; a container fed by an env-file was not recreated when that file changed (issue 103, since fixed in the host); and some secrets are read only when a service first initialises, where a restart changes nothing.
And providers cannot answer with data. ADR 0048 left "delivering provider-generated data back to a consumer" to a separate decision. The analytics provider's site id and the DNS provider's record have no way back, and say so in their code.
Considered Options
1. Keep the controller minting, and tidy the paths. Rejected. The paths are the problem: each is made, kept, rotated and audited differently, and tidying keeps them all.
2. Every provider mints its own secrets, with one shared function in the SDK. Rejected. Generation becomes uniform, but custody stays spread over every provider's machine, so rotation, audit and the operator's break-glass copies cover only some secrets. Each SDK language needs its own implementation.
3. Raise the vault first at genesis, so it makes even the first secrets. Rejected. The vault is built on the shared runtime base, which the installation makes only after the store, the broker and the controller exist, and the vault learns what to answer from the controller over the bus. Running it first means reordering the whole installation and giving the vault a second way of being asked.
4. The vault makes every shared secret; genesis delivers the first ones to it. Chosen. It answers 0085's objection with a mechanism the mesh already has: a value delivered to the vault.
Decision
There are two kinds of secret, and each has one rule.
- A shared secret is a value more than one party must hold: a password, a token, an API key. The vault makes every one. Nothing else in the mesh generates a shared secret.
- A private key is made where it is used and never leaves: a node's sealing key, the operator's key, the mesh's certificate authority. This is not a second way of making secrets. A private key any other party ever held would no longer be private.
Every shared secret is a secret requirement, answered by the vault:
- a credential between a consumer and a provider. A provision's contract declares for each consumer, one secret, and resolution expands it into one requirement per consumer. So gitea requiring a database makes the database's provider require a secret for gitea, and the vault answers it. The provider's own code does not change: it is handed a login and a password, as today;
- a module's own secret.
own-secretsis retired; - every broker account on the mesh's bus: a module's, a node's host's, the builder's, the
controller's. The broker holding
mesh-brokercarries the mesh's bus (ADR 0110), and its own provisioner creates each account from the vault's secret, like any provider. The controller no longer creates accounts, and there is no separate command to forget; - an enrolment token. The vault makes it; the operator receives the token, sealed to the operator key, to hand to the joining machine; the controller receives only what it needs to verify it, never the token itself;
- a secret operator value, such as an external API key, which the operator delivers to the vault (ADR 0092). A licence's credential is one of these. What the licences context adds, refreshing a token, is provider behaviour, decided in its own record;
- a secret a backend issues itself, such as an API token a forge hands out exactly once when asked. The vault cannot make that value. The module that received it delivers it to the vault, which keeps it and provides it like any other; rotating it means asking the backend again.
The controller is a module, and takes the same path. Its store logins (inventory, identity and
licences) and its bus accounts are own secrets of its definition today, and become secret requirements
of that definition like any module's. A node's host is the one party with no definition. Its bus
account is a requirement the mesh makes for each enrolled node, answered by the vault, sealed to that
node and carried like any other. It is the only requirement not written in a definition, because the
host is what runs definitions.
Only the vault may provide secret. An assignment providing it must hold the mesh-vault seat.
The parser refuses a definition that provides it and cannot hold the seat, and a pin cannot route a
secret requirement anywhere else, because there is nowhere else.
A secret has recipients, and the vault delivers to each. The database credential has two: the provider, which applies it by creating the login, and the consumer, which reads it and presents it when it connects. The vault hands the value to the mesh sealed to each recipient's node. The controller and the broker carry sealed values they cannot open.
Genesis delivers, and the vault adopts. The vault is built on the shared runtime base, which the installation makes only after the store, the broker and the controller are running (to-be 21). So the vault is installed as soon as that base exists, before any other module built on it, and everything needed before that moment is generated by genesis:
- the store's superuser, and the broker's admin in the hashed form the broker needs;
- the bus accounts of the temporary and permanent controller (its account and the broker-management login), the control-node's host, the builder, the broker's own provisioner and the vault;
- the controller's three store logins (inventory, identity and licences), and the first enrolment token.
Until the broker's provisioner runs, genesis creates the bus accounts it generated, with the broker's admin, as the controller does today. Genesis seals each value twice: to the control-node's key, so that when the vault is installed the controller delivers the values to the vault, recorded as the mesh's own, not as an operator's, with nobody present; and to the operator key, as the break-glass copy ADR 0085 keeps of every root secret. The first enrolment token reaches the operator the same way. That distinction matters: an operator's value is never replaced (ADR 0092), and these are, because the vault can make their replacements. The broker's provisioner then adopts the accounts genesis created. From then on the vault makes every shared secret, and genesis has made its last one.
Raising the vault or the broker again is a genesis act. Moving the mesh-vault or mesh-broker
seat to a new assignment, or recovering either after it is lost, is done the way genesis did it: the
values it needs are delivered, not made by a vault that is not there. They come from the operator-sealed
copies, which the operator opens. The vault keeps a copy of every secret sealed to the operator key
(0085), so nothing the mesh relies on exists only inside the vault. That is a break-glass procedure,
stated and checked, never an ordinary assignment.
A provider makes resources and data, and the mesh carries data back. A provider's adapter may answer with its contract's non-secret fields: a site id, a registered name. The mesh delivers them to the consumer as resolved values. Who a consumer is stays the mesh's: a provider makes what a consumer is given, never what it is called (ADR 0049).
Rotation
Who asks and who makes are decided here; the mechanism is not. A rotation is asked of the vault, by an operator or by the vault's policy, such as a maximum age in the requirement's contract, and the vault makes the new value. A delivered value the vault cannot replace, such as an external API key, is not rotated by the vault: rotating it means an operator delivering a new one. A secret a backend issued is rotated by the module that holds the backend asking it again and delivering the new value to the vault.
Each recipient takes a new value one of two ways, marked per recipient:
| recipient takes it by | example | what happens on rotation |
|---|---|---|
| applying it | a provider setting a login's password; the broker's provisioner updating an account; a store's provisioner changing its own superuser | its provisioner applies the new value; it is never restarted for it |
| reading it at start | a consumer reading its password when it starts | the host recreates it, because a file it read at creation changed |
The marking is per recipient, not per secret, because one secret has recipients of both kinds. A provision's contract marks its provider's side, which applies. A consumer's side is read at start unless its requirement says otherwise. The broker's contract marks the host's bus account the same way: the broker's provisioner applies it, and the host reads it. Every module in the catalogue reads its secrets at start, and none watches them (research 016). A secret a backend takes only when it first initialises is marked applied, and its provider's provisioner makes the change using the old value. Where no provisioner can make it, the requirement is marked not rotatable by the mesh, and a rotation request is refused, saying why, rather than restarting a service that would carry on with the old value.
How old and new change over is decided in ADR
0114. Three mechanisms were measured against
every provider's code in research
016: in place, as the controller's
rotate does today; two secrets on one login; and two logins over one resource. Its findings bound the
choice:
- every credential provider already re-applies a password in place, so today's rotation works, with a window in which a consumer cannot authenticate;
- eight of nine name the consumer's resource after its login, and five destroy the consumer's data when they remove the login. No mechanism may retire a login through today's remove, because in those five it deletes the consumer's data;
- one backend holds two passwords on one login, and two more hold several tokens;
- every provider can hold two credentials over one resource, eight as two logins and one as two tokens, once the adapter separates the resource from the credential.
On those facts, 0114 rotates a credential two parties hold over two credentials, rotates one a single party holds in place, staged, and separates retiring a credential from removing a consumer.
What this changes in earlier records
On acceptance, each of these is superseded or amended by this record, not edited:
- ADR 0048 is superseded: the controller no longer mints a provider's credential; the vault makes it. That a provider is handed its credential and seals nothing stands.
- ADR 0085 is amended: the vault makes every shared secret, own secrets are retired, and its rejection of vault-only minting is answered by genesis delivering the first secrets. Genesis seals its values to the control-node's key as well as to the operator key, so the controller can deliver them unattended. "The vault stores no plaintext, ever" and the operator-sealed break-glass copies stand.
- ADR 0092 is amended: an operator delivers a secret to the vault. Genesis's values reach the vault by delivery too, but are recorded as the mesh's own, so 0092's rule that an operator's value is never replaced does not apply to them.
- ADR 0043 is amended: a broker account is created by the broker's provisioner, not the controller. Its scoping stands.
- To-be 13, to-be 21 and to-be 24 are amended: the vault makes a rotated value and each requirement says whether its recipient applies it or reads it at start, and the changeover is ADR 0114's; the vault is installed as soon as the shared runtime base exists, and genesis delivers its secrets to it; the vault is the only maker.
- To-be 07 is amended: genesis seals its values to the control-node's key as well as the operator key.
- To-be 12, to-be 16
and to-be 18 are amended:
own-secretsis retired from the manifest they describe. - The glossary gains shared secret, recipient, applies and reads at start, and reserved provision, once this record is accepted.
- Issue 103 is a prerequisite, and its fix is in the host: a container is recreated when a file it read at creation changes. The issue is to be recorded as fixed, and derived restarts rest on it.
Consequences
- The vault is on the path of every new or rotated shared secret. Today the controller holds that place, on the same node. A secret can no longer be made while the vault is down.
- Resolution expands per-consumer requirements from a provision's contract. The contract declares them, never the provider's code, so what a provider requires stays predictable from the catalogue.
- A data provider's adapter gains a return value. What a credential provider's adapter must change for rotation is ADR 0114's.
- The broker's provisioner gains every bus account, and the controller loses five separate places it generates a secret today.
- 54 modules move from own secrets to vault requirements. Six provider clients export a password generator nothing uses any more; it is removed, so no module can quietly start minting again.
- The installation changes order: the vault is installed as soon as the shared runtime base exists, before any other module built on it.
- What got harder: a secret some services read only at first start can no longer be "rotated" by a restart that quietly changes nothing; it is refused instead, or applied by its provisioner. And moving the vault or the broker is a procedure, not an assignment.
How it is checked
| Rule | Checked by |
|---|---|
| Only the vault generates a shared secret after genesis | A controller test: no code path generates a shared secret. A catalogue test: no module's code generates one, found by scanning for generation calls. Exempt are the vault itself, and randomness that is not a secret any other party holds, such as a password hash's salt, each named in a declared list. An installer test: genesis generates exactly the list above and delivers it to the vault, recorded as the mesh's own. |
| A private key is made where it is used | A test per key: a node's sealing key never leaves the node, the operator's private key never enters the mesh, and the certificate authority's private key never leaves the controller's identity store. |
Only the vault provides secret |
The parser refuses a definition providing secret that cannot hold mesh-vault, and resolution refuses a pin on a secret requirement. |
| The controller and each node's host take the same path | A catalogue test: the controller's definition declares no own secret, only requirements. A controller test: a node's bus account is made by the vault and delivered sealed to that node; an enrolment token reaches the controller only as what verifies it. |
| Genesis's values reach the vault unattended, and the operator keeps a copy | An installer test: each of genesis's values is sealed to the control-node's key and to the operator key; the controller delivers the first to the vault when it is installed, with no operator step; the operator's copy opens only with the operator key. |
| Moving the vault or broker is a procedure | A resolution test: an ordinary assignment moving mesh-vault or mesh-broker is refused, naming the procedure. |
| A backend-issued secret enters through the vault | A vault test: a value delivered as issued is provided like any other, and rotating it is refused as the vault's act. |
| A secret with no provisioner to apply it is not rotated by restart | A vault test: rotating a secret whose requirement is marked not rotatable by the mesh is refused, naming why. |
| A rotation never destroys a consumer's data | A provider test per credential provider: rotating a consumer's credential leaves its resource and data intact. It fails today for no provider, because rotation is in place; it guards whichever mechanism replaces it. |
| A provider's per-consumer secret comes from the vault | A resolution test: a consumer requiring a database expands to a secret requirement for it, answered by the vault and delivered to both recipients. |
| Values are carried sealed | A controller test: each recipient's copy opens with that recipient's node key and no other; neither the controller nor a message on the broker can open one. |
| Own secrets are retired | A catalogue test: no definition declares an own secret, with a declared list of exceptions that shrinks to empty. |
| Bus accounts come from the broker's provisioner | A resolution test: assigning a module that speaks on the bus yields its account, created by the broker's provisioner with no separate command. |
| An operator's value is never rotated by the vault, and genesis's values are | Vault tests: a rotation request on an operator's external key is refused, naming the operator; the same request on a value genesis delivered makes a replacement. |
| Restarts are derived from how a secret is read | A host test: a secret read at start recreates the container that read it at creation, through an env-file or a direct mount; an applied secret restarts nothing. A catalogue test: a secret that reaches a process, or a file in a mounted directory, has restart-on naming it. |
| A provider answers data back | A lab test with a consumer requiring analytics: the provider's site id reaches it as a resolved value. |
References
- ADR 0048: the decision this supersedes, and the return path it left open
- ADR 0085: the vault, and the objection this record answers
- ADR 0092, ADR 0049, ADR 0043: delivered values, identity, and broker accounts
- ADR 0112, to-be 27: everything a module needs is a requirement
- Issue 095, issue 103: what fails today