ADR 0113 and to-be 27: address the review of the vault rework

Two decisions taken with the author:
- Genesis delivers and the vault adopts. The vault cannot run first — it is built on the runtime base
  the installation makes after the store, broker and controller, and it learns its work over the bus.
  Genesis generates the foundation's first shared secrets, seals them to the operator key, and
  delivers them to the vault through the path an operator's value takes; from then on the vault holds
  and rotates them. This answers ADR 0085's own reason for rejecting vault-only minting, which 0113
  now names instead of stepping around.
- Rotation re-confirms on every pass. An applier repeats its confirmation until acknowledged, so a lost
  message costs one pass; an applier that stops after applying locks readers out until its supervised
  restart, and that window is stated and shown, not claimed away.

Fixes:
- Scope: a shared secret is made by the vault; a private key (node sealing keys, the operator's key,
  the certificate authority) is made where it is used. The inventory adds the makers the first version
  missed: node and builder broker passwords, and enrolment tokens.
- Broker accounts are created by the broker's provisioner, not the controller, so the controller never
  holds their plaintext; mesh-broker delivers amqp again — one broker per mesh — and only mesh-store
  delivers nothing.
- secret is a reserved provision: only the mesh-vault holder may provide it, and no pin routes around it.
- A secret's contract says whether a recipient applies it or reads it at start; appliers are never
  restarted for it, init-only secrets are applied, and confirmation is to-be 13's standard.
- Operator secrets are one rule everywhere: a secret requirement answered by the vault (0112 no longer
  says otherwise). A data provider's adapter may return fields; the data-return check names a lab consumer.
- 'Holder' now means a seat's holder only; a secret has recipients.
This commit is contained in:
jochen
2026-09-25 23:27:58 +02:00
parent fa2c09a2c5
commit 1b5f2c2c1a
7 changed files with 253 additions and 165 deletions
+4 -3
View File
@@ -57,9 +57,10 @@ consumer's own machine does not take over for that consumer. That is not picking
once, mesh-wide, by assigning the holder, rather than once per consumer by naming it
([ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md),
[26 — The seats](26-the-seats.md)). A named provider still wins over the seat, because a consumer
coupled to particular contents has said so. Only provisions the design makes one-per-mesh are
delivered by a seat: the artifact store, a package registry, git and the vault. A database is not.
Node-local stores, served by co-location, are the rule above.
coupled to particular contents has said so, except for `secret`, which only the vault may provide.
Only provisions the design makes one-per-mesh are delivered by a seat: the broker, the artifact store,
a package registry, git and the vault. A database is not. Node-local stores, served by co-location,
are the rule above.
**Ambiguity is refused, never resolved by picking.** If several providers of a kind exist, none is
named, none is co-located, and no seat delivers it, the requirement is unsatisfiable and is refused
+15 -8
View File
@@ -48,8 +48,8 @@ argued for is an entry nobody can explain.
|---|---|---|---|
| `mesh-controller` | mesh | — | the controller |
| `mesh-store` | mesh | — | the foundation's store |
| `mesh-broker` | mesh | — | the foundation's broker |
| `mesh-vault` | mesh | `secret` | the vault |
| `mesh-broker` | mesh | `amqp` | the broker |
| `mesh-vault` | mesh | `secret`, reserved | the vault |
| `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
| `the-catalogue` | mesh | — | the catalogue |
| `npm-package-registry` | mesh | `npm-package-registry` | the forge |
@@ -64,8 +64,8 @@ argued for is an entry nobody can explain.
The controller holds this set in code, and a test asserts both its size and that every entry names
the record that made it a seat. **This table and [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)
govern, and code that disagrees is what is wrong.** The implementation in progress predates two
things here: the `mesh-vault` seat, and the rule that `mesh-store` and `mesh-broker` deliver
govern, and code that disagrees is what is wrong.** The implementation in progress predates three
things here: the `mesh-vault` seat and its reservation, and the rule that `mesh-store` delivers
nothing. It is brought to this table before it merges.
## A seat that delivers a provision
@@ -73,11 +73,18 @@ nothing. It is brought to this table before it merges.
A seat that delivers a provision may only be held by a module that provides it, at the seat's scope.
A mesh seat delivers a mesh-scoped provision.
**A seat delivers a provision only where the mesh has one answer for everyone.** The artifact store,
the npm registry, git and the vault are each one per mesh by decision. The store and the broker are
**A seat delivers a provision only where the mesh has one answer for everyone.** The broker, the
artifact store, the npm registry, git and the vault are each one per mesh by decision. The store is
not: nodes run their own stores and a consumer uses the one on its machine
([23 — Choosing a provider](23-choosing-a-provider.md)). So their seats guard that the foundation's
own server is singular, and route nobody.
([23 — Choosing a provider](23-choosing-a-provider.md)), and the foundation's store is the controller's
own memory, provider to nobody. So `mesh-store` guards that the foundation's store is singular, and
routes nobody.
**The vault's provision is reserved.** Only the holder of `mesh-vault` may provide `secret` at all: a
module providing it without the seat is refused, and a pin cannot choose another provider, because
there is none. A second provider of secrets would be a second place secrets live, which is what the
vault being one per mesh exists to prevent. Every other delivered provision may have second
providers, which a pin can choose.
**Its holder answers for that provision.** A requirement for it resolves, in order, to:
@@ -71,9 +71,17 @@ Which module answers, in order:
### 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.
**Two kinds of secret, one rule each** ([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)):
- **a shared secret**, a value more than one party must hold (a password, a token, an API key), is
made by the vault, and by nothing else;
- **a private key**, such as a node's sealing key, the operator's key or the mesh's certificate
authority, is made where it is used and never leaves. A private key anyone else held would no
longer be private.
**Every shared secret is a `secret` requirement, and only the vault provides `secret`.** The vault
holds the `mesh-vault` seat, and that provision is reserved to it: no other module may provide it, and
no pin can choose another provider ([26 — The seats](26-the-seats.md)).
**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
@@ -82,16 +90,19 @@ 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;
4. the mesh delivers it to both of its **recipients**, each sealed to its own node: the database's
machine, which *applies* it by creating the login, and gitea's, which *presents* 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.
Every other shared secret takes the same path:
- a module's own secret;
- every broker account's password, where the broker's own provisioner creates the account;
- an enrolment token;
- a secret operator value, which the operator delivers to 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.
**A provider makes resources and data.** Beyond secrets, a provider's adapter may answer 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
@@ -180,36 +191,54 @@ slug, under the same rules as a slug. This has to be settled before a second ins
## Genesis
**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.
**Genesis delivers, and the vault adopts.** The vault cannot run first: it is built on the shared
runtime base, which the installation makes only after the store, the broker and the controller exist
([21 — The installation in full](21-the-installation-in-full.md)), and it learns what to answer from
the controller over the bus. So genesis generates the foundation's first shared secrets itself:
- the store's superuser;
- the broker's admin, in the hashed form the broker needs;
- the bus accounts of the temporary controller and of the vault;
- the first enrolment token.
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.
It seals them to the operator key as today ([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md)).
When the vault is installed, genesis **delivers them to it**, through the same path an operator's value
takes. From then on the vault holds, audits and rotates them. It can make their replacements, unlike
an operator's external key.
That is the one time anything but the vault generates a shared secret, and it ends by handing them
over. It is also the answer to the objection ADR 0085 had to the vault being the only maker: the
vault cannot make what exists before it, so what exists before it is delivered to it.
## 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)).
An operator's external key is not rotated by the vault, which cannot make its replacement: an
operator delivers a new one.
**A secret's contract says how each recipient takes a new value.** A recipient either *applies* it,
through a provisioner (a provider creating the login, the broker's provisioner updating an account,
the store's own provisioner changing its superuser), or *reads it at start*. A secret a service reads
only when it first initialises is marked applied, because a restart would change nothing.
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)
2. **It goes first to the recipients that apply it.** Each applies it, verifies that the new value
authenticates and the old one no longer does, and confirms. It repeats that confirmation on every
reconcile pass until the vault acknowledges it, so a lost message costs one pass.
3. **Only then is it released to the recipients that read it at start**, such as gitea.
4. **The host restarts every such recipient**, and recreates a container whose env-file carries the
secret. It knows which, because a definition reads a secret only through its requirement, so no
definition declares a restart for a secret. An applying recipient is never restarted for it.
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.
5. **It is confirmed.** The rotation shows as unconfirmed until every applier has confirmed, and every
recipient that reads at start has restarted and passed its health check, where its definition
declares one. Delivered and working are shown as different things.
**The remaining window is stated.** An applier whose provisioner stops after applying and before
confirming leaves the recipients that read at start locked out: the old value no longer works, and
they have not been sent the new one. A provisioner is supervised and restarted when it exits, so the
window lasts until that restart. The rotation shows as waiting on that applier throughout, never as done.
## Refusing
@@ -250,12 +279,14 @@ 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. **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.
2. **The vault makes every shared secret, and providers answer.** The vault holds its seat and its
reserved provision; resolution expands per-consumer secret requirements; genesis delivers the
foundation's first secrets to the vault; the broker's provisioner creates every bus account; 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* nothing outside the vault generates a shared secret after genesis, a lab
consumer of analytics receives its site id, and a database credential rotates applier-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.
@@ -277,9 +308,10 @@ 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. |
| Only the vault generates a shared secret after genesis | A controller test: no code path generates one. An installer test: genesis generates exactly the foundation's first secrets and delivers them to the vault. |
| Only the vault provides `secret` | The parser refuses another provider of it, and resolution refuses a pin on a `secret` requirement. |
| 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 recipients. |
| Rotation is applier-first, derived and confirmed | The rotation tests of [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md): readers wait for every applier's repeated confirmation; an applied secret restarts nothing, and one read at start restarts its reader without a declared restart; the rotation shows unconfirmed until the new value authenticates, the old does not, and readers are 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. |