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.
12 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 secret, a provider makes resources and data, and the mesh carries both
Context
A secret comes into being seven 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 | 19 modules |
a module's own secret (own-secrets) |
the controller, as a random value nothing owns | 54 modules |
| a 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 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 mesh'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. The same has happened to the broker account, a special case of the second row that fails silently when the separate command is forgotten.
Rotation has its own gaps. To-be 13 makes rotation one command, all-or-nothing, and states the window in which a consumer cannot authenticate: the provider has taken the new password, and the consumer has not yet restarted with it. A consumer restarts only if its definition remembered to say so, and a container fed by an env-file is not recreated when that file changes, so it keeps the old value (issue 103). Nothing confirms that the new secret works.
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 seven paths. Rejected. The paths are the problem: each is made, kept, rotated and audited differently, and tidying keeps all seven.
2. Every provider mints its own secrets, with one shared function in the SDK. Rejected. It makes generation uniform and leaves custody scattered: every provider's machine holds secrets the vault never sees, so rotation, audit and the operator's break-glass copies cover only some of them. The function would also need a conforming implementation in every language a provider is written in.
3. The vault makes every secret, and a provider that needs one requires it, like any consumer. Chosen. A provider serving a consumer requires a secret for that consumer from the vault. Secrets become provisioning all the way down, with one maker at the bottom.
Decision
The vault makes every secret in the mesh. Generation, to a secret's contract, exists in the vault and nowhere else. The controller mints nothing.
A provider that needs a secret for a consumer requires it from the vault. A provision's contract declares it: for each consumer, one secret. Resolution expands that into one requirement per consumer, named for the consumer. So gitea requiring a database makes the database's provider require a secret named for gitea, and the vault answers it. The provider's own code does not change. It is handed a login and a password, as it is today.
A secret has holders, and the vault delivers to each. The database credential has two: the provider, which creates the login with it, and the consumer, which presents it. The vault hands it to the mesh, which delivers it to each holder sealed to that holder's node. Plaintext exists in the vault while it is made and on each holder's machine, and nowhere else. The controller carries sealed values it cannot open.
Every other secret takes the same path:
- a module's own secret is a
secretrequirement the vault answers.own-secretsis retired; - a broker account is the module's identity on the bus. Its name is the mesh's, its password is a secret the vault makes, and the controller creates the account with it, as it creates accounts today. There is no separate command to forget;
- an operator's value that is secret is handed to the vault, which provides it like any other secret. It is never replaced by rotation (ADR 0092);
- a licence's credential is an operator's value the vault keeps. What the licences context adds, refreshing a token and a manager holding the refresh credential, is provider behaviour, decided in its own record.
Genesis is the vault's first answer, not an exception. Genesis raises the vault before anything else and asks it for the foundation's secrets: the store's superuser, the broker's admin and its hashed form, and the vault's own broker account. The vault answers with the same code it always uses, before the bus exists. Genesis mints nothing itself.
A provider makes resources and data, and the mesh carries data back. A provider answers 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
It is asked of the vault, by an operator or by the vault's own policy, such as the maximum age a secret's contract sets.
It is provider-first. The vault delivers the new value first to the holders that accept it,
such as the database, and waits for each to confirm it has applied it. Only then does it release the
value to the holders that present it, such as gitea. A consumer is never sent a value its provider
has not accepted, so the window shrinks to the consumer's own restart. A provider that does not
confirm holds the rotation: the consumer keeps the old value, which still works, and status shows the
rotation as waiting on that provider. It is never half-done and never silently abandoned.
Restarts are derived, not declared. The mesh knows which process reads which secret, because the
definition reads it through its requirement (to-be 27).
When a secret changes, the host restarts every process that reads it, and recreates a container whose
env-file carries it. No definition has to remember restart-on for a secret.
It is confirmed. A rotated secret is shown as unconfirmed until each consumer has restarted with it and, where its definition declares a health check, passed it. Delivered is not the same as working, and the mesh says which one it knows.
| step | who |
|---|---|
| asks | an operator, or the vault's policy |
| makes the value | the vault |
| carries it | the controller, sealed, provider first |
| applies it on the provider | the provider's provisioner, which confirms |
| applies it on the consumer | the host, restarting or recreating what reads it |
| confirms it works | the consumer's restart and health check, shown in status |
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 a module's own secret rather than recording one the controller minted, own secrets are retired, and genesis asks the vault for the root secrets. "The vault stores no plaintext, ever" stands.
- ADR 0092 is amended: an operator delivers a secret to the vault.
- To-be 13 and to-be 24 are amended: rotation is provider-first, derived and confirmed, and the vault is the only maker.
- Issue 103 becomes a prerequisite: rotation cannot be trusted while a changed env-file leaves a container on its old value.
Consequences
- The vault is on the path of every new or rotated secret. Today the controller is, and both run on the control-node, so no new single point of failure appears. It is stated rather than implied.
- 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.
- The SDK's provider loop gains one thing: confirming that a rotation was applied. Adapters are unchanged.
- The mesh gains the way back from provider to consumer, for confirmations and for data.
- 54 modules move from own secrets to vault requirements, and the broker account stops needing a separate command.
- What got harder: a secret can no longer be made when the vault is down, where today the controller makes one regardless. And a rotation waits for its provider. Both move failures from late and silent to early and visible, which is the trade this record makes throughout.
How it is checked
| Rule | Checked by |
|---|---|
| Only the vault makes secrets | A controller test: no code path mints a secret. A vault test: the one generation function is the only one, and genesis reaches it through the vault. |
| A provider's per-consumer secret comes from the vault | A resolution test: a consumer requiring a database expands to a secret requirement named for it, answered by the vault. |
| A secret reaches each holder sealed to its node | A controller test: each holder's copy opens with that holder's node key and with no other, the controller's included. |
| Own secrets are retired | A catalogue test: no definition declares an own secret, with a declared list of exceptions that shrinks to empty. |
| A broker account needs no separate command | A resolution test: assigning a module that speaks on the bus yields its account. |
| Rotation is provider-first | A rotation test: the consumer is not sent the new value until the provider confirms; a provider that never confirms leaves the consumer on the old value, shown as waiting. |
| Restarts are derived | A host test: changing a secret restarts every process that reads it, and recreates a container whose env-file carries it, with no restart-on declared. |
| Rotation is confirmed | A rotation test: the secret shows as unconfirmed until the consumer restarted and passed its health check. |
| A provider answers data back | A resolution test: the analytics provider's site id reaches its consumer as a resolved value. |