ADR 0113 and to-be 27: rotation overlaps old and new credentials

Decided with the author. A credential is never changed in place: each consumer has two logins, both
derived by the mesh, and uses one at a time. An applier adds the new login beside the old through the
adapter's existing create, and confirms both work; only then are readers released to the new one and
restarted by derivation; only when every reader has confirmed is the old login retired through the
existing remove.

It closes the three cases review found in applier-first rotation: an offline reader keeps working on
the old login until it returns; a bus account's owner keeps its bus until it has moved; a provisioner
restarted mid-rotation is still delivered both values. Nobody is ever without a credential that works,
which replaces to-be 13's all-or-nothing rule with a stronger one.

No consumer module changes. The alternation is the provider loop's. A provider's adapter gains one duty,
giving both logins the same rights over the consumer's data — in postgres, membership of one role that
owns it. The mesh derives two logins per consumer, both within ADR 0049's limit, which 0113 now names
among what it amends. Every rule has a check: overlap, offline reader, bus account, restarted
provisioner, equal rights, login length, and confirmation only once the old login is gone.
This commit is contained in:
jochen
2026-09-25 23:50:20 +02:00
parent 4a1b218706
commit 805df3f81e
2 changed files with 88 additions and 58 deletions
@@ -153,39 +153,55 @@ the old value. The host derives which recipients read a secret at start from the
definition reads it through, so no definition declares a restart for a secret. A provider's
per-consumer secrets are applied, never read at start, so the host never restarts a provider for one.
**It is applier-first.** The vault delivers the new value 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. Only after every applier has confirmed does the vault release the value to the
recipients that read it at start.
**Old and new overlap: nobody is ever without a credential that works.** A credential is never
changed in place. The new one is added beside the old, every reader moves to it, and only then is the
old one removed. There is one mechanism, the same for every provider:
**Open: keeping readers from being locked out.** Review found three cases this rule does not survive:
1. **The vault makes the new value.**
2. **Each applier adds it beside the old.** A consumer has two logins, both derived by the mesh, and it
uses one at a time. The provider's loop creates the other with the new value, through the adapter's
existing create, and leaves the one in use untouched. It verifies that the new login works and the
old one still 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 the new login released to the readers.** A reader receives the new login and its
value together. The host restarts it, or recreates a container whose env-file carries it.
4. **Each reader confirms**, by restarting with the new login and passing its health check, where its
definition declares one.
5. **Only when every reader has confirmed is the old login retired.** Each applier removes it, through
the adapter's existing remove, and verifies that it no longer authenticates.
- a reader whose machine is offline when an applier has already applied the new value is locked out
until it returns, where to-be 13 would have refused the rotation and kept the old value working;
- a bus account's owner can be locked out permanently, because the confirmation and the new value
travel over the bus it has just lost;
- a provisioner restarted mid-rotation no longer knows the old value, so it cannot verify that the old
value has stopped working.
**What overlap closes:**
Two answers are recorded, and one must be chosen before this record is accepted. **Overlap:** an
applier keeps the old and new credential valid together until every reader has confirmed the new one,
for instance by alternating between two derived logins with the adapter's existing create and remove,
which needs no change on the consumer's side. Or **re-confirm with safeguards:** a pre-check that every
reader is reachable before any applier starts, plus a special path for bus accounts.
- a reader whose machine is offline keeps the old login, which still works, until it returns and
moves; the rotation shows as waiting on that reader, and nobody is locked out;
- a bus account's owner keeps its old account until it has confirmed the new one over the bus it still
has, so no party can lose the bus it would hear the new value on;
- a provisioner restarted mid-rotation is still delivered both values until the old is retired, so it
can verify either.
**It is confirmed.** A rotation is shown as unconfirmed until every applier has confirmed, and every
recipient that reads at start has restarted with the new value and passed its health check, where its
definition declares one.
**What overlap costs.**
- **Consumer modules: nothing.** A consumer reads one login at a time and changes it when it restarts.
- **Providers: one duty.** Both of a consumer's logins must have the same rights over its data,
because the consumer's data was written under one login and is read under the other. In postgres,
both are members of one role that owns the data. That is the adapter's part, and the only place
overlap touches provider code. The alternation itself is the provider loop's, so every provider gets
it by using the harness.
- **The mesh:** it derives two logins per consumer, and both must still fit the tightest backend
([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md)).
- **Secrets with no applier**, such as a module's own secret read only by itself, have no second party
to overlap with. They are delivered and the reader restarted, where their contract allows rotation at
all.
| step | who |
|---|---|
| asks | an operator, or the vault's policy |
| makes the value | the vault |
| carries it | the controller, sealed, appliers first |
| applies it | each applier's provisioner, which verifies and confirms on every pass until acknowledged |
| takes it at start | the host, restarting or recreating what reads it |
| confirms it | applier confirmations, then restarts and health checks, shown in `status` |
| adds the new login beside the old | each applier's provisioner, confirming on every pass until acknowledged |
| moves each reader | the host, restarting or recreating what reads the secret |
| confirms each reader | its restart and health check |
| retires the old login | each applier's provisioner, once every reader has confirmed |
| shows progress | `status`: waiting on which applier or reader, never done until the old is retired |
## What this changes in earlier records
@@ -202,11 +218,14 @@ On acceptance, each of these is superseded or amended by this record, not edited
so 0092's rule that an operator's value is never replaced does not apply to them.
- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) is amended: a broker
account is created by the broker's provisioner, not the controller. Its scoping stands.
- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md) is amended: the mesh derives two
logins per consumer, and both fit the tightest backend.
- [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md),
[to-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md) and
[to-be 24](../03-DESIGN/01-to-be/24-the-secrets-vault.md) are amended: rotation is applier-first,
derived and confirmed; 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 24](../03-DESIGN/01-to-be/24-the-secrets-vault.md) are amended: rotation overlaps old and new
instead of being all-or-nothing, with restarts derived and each step confirmed; 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.
- The [glossary](../00-META/glossary.md) gains *shared secret*, *recipient*, *applies* and *reads at
start*, and *reserved provision*, once this record is accepted.
- [Issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)
@@ -219,17 +238,19 @@ On acceptance, each of these is superseded or amended by this record, not edited
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.
- The SDK's provider loop gains repeated confirmation of an applied rotation. A credential provider's
adapter is unchanged. A data provider's adapter gains a return value.
- The SDK's provider loop gains the alternation of two logins per consumer and repeated confirmation
of each step. A credential provider's adapter gains one duty, giving both logins the same rights over
the consumer's data. A data provider's adapter gains a return value. No consumer module changes.
- 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 rotation waits for its appliers, and how a reader is kept from being locked
out while it does is still open (above). 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.
- **What got harder:** a rotation lasts until its slowest reader has moved, so a reader offline for a
week keeps the old login valid for a week. That is shown, and it is the price of never locking anyone
out. A provider briefly holds two logins per consumer. 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.
## How it is checked
@@ -246,9 +267,14 @@ On acceptance, each of these is superseded or amended by this record, not edited
| 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. |
| Rotation is applier-first | A rotation test: readers are not sent the new value until every applier confirms. How lock-out is prevented is open, and its check is written when that is decided. |
| Old and new overlap | A rotation test: after an applier adds the new login, both authenticate; readers are released only after it confirms; the old login is removed only after every reader confirms, and then no longer authenticates. |
| An offline reader is never locked out | A rotation test with one reader's node offline: it keeps authenticating with the old login throughout, the rotation shows waiting on it, and completes when it returns. |
| A bus account's owner keeps the bus | A rotation test on a node agent's bus account: the agent stays connected on the old account until it has confirmed the new one. |
| A restarted provisioner can still verify | A rotation test restarting the applier's provisioner mid-rotation: it is delivered both values and confirms. |
| Both logins have the same rights | A provider test per credential provider: data written under one of a consumer's logins is read and changed under the other. |
| Both logins fit the tightest backend | A controller test: the two derived logins for the longest node and module names fit the limit ADR 0049 sets. |
| Restarts are derived from how a secret is read | A host test: a secret read at start restarts its reader, and recreates a container whose env-file carries it; an applied secret restarts nothing. |
| Rotation is confirmed | A rotation test: an applier confirms only after the new value authenticates and the old one does not, and the rotation shows unconfirmed until every reader restarted and passed its health check. |
| Rotation is confirmed | A rotation test: the rotation shows unconfirmed until every reader has restarted with the new login and passed its health check, and the old login is retired. |
| 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
@@ -233,26 +233,32 @@ through a provisioner (a provider creating the login, the broker's provisioner u
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 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 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.
**Old and new overlap, so nobody is ever without a credential that works.** A credential is never
changed in place. Each consumer has two logins, both derived by the mesh, and uses one at a time:
**Open: keeping readers from being locked out.** Steps 2 and 3 leave a reader locked out when its
machine is offline after an applier applied, when the secret is a bus account whose owner loses the bus
it would hear the new value on, or when a restarted provisioner can no longer check the old value.
[ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md) records the two answers, overlapping
old and new credentials or re-confirming with safeguards, and one is chosen before it is accepted.
Neither changes a consumer module.
1. **The vault makes the new value.**
2. **Each applier adds it beside the old**, as the consumer's other login, through the adapter's
existing create. It verifies that the new login works and the old one still does, and confirms,
repeating that confirmation on every reconcile pass until the vault acknowledges it.
3. **Only then is the new login released to the readers**, such as gitea, login and value together.
4. **The host restarts every such reader**, 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 applier 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. **Each reader confirms** by passing its health check with the new login, where its definition
declares one.
6. **Only when every reader has confirmed is the old login retired**, through the adapter's existing
remove, and verified to no longer authenticate.
So a reader whose machine is offline keeps working on the old login until it returns, a bus account's
owner keeps its bus until it has moved, and a provisioner restarted mid-rotation is still delivered
both values. The rotation shows as waiting on whichever applier or reader has not moved, and is done
only when the old login is gone.
**No consumer module changes.** A provider's adapter gains one duty: both of a consumer's logins get the
same rights over its data, which in postgres means both belong to one role that owns it. The
alternation itself is the provider loop's.
A secret some service reads only when it first initialises cannot be rotated by restarting it. It is
applied by a provisioner, or, where none exists, marked not rotatable by the mesh, and a rotation is
@@ -304,8 +310,8 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
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.
consumer of analytics receives its site id, and a database credential rotates with old and new
overlapping, the consumer restarted by derivation and the rotation confirmed.
3. **Definitions move, and seats move to assignments.** Every catalogue definition is rewritten, adopted
and running assignments placed where their data already is, and each claim becomes a seat the
module can hold, held by the assignment that holds it today. *Ends when* the list of definitions
@@ -331,7 +337,7 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
| 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. |
| Rotation overlaps old and new | The rotation tests of [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md): both logins authenticate while readers move; an offline reader keeps working on the old login; the old login is retired only after every reader confirms; an applied secret restarts nothing, and one read at start restarts its reader without a declared restart. |
| 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. |
@@ -341,5 +347,3 @@ Each phase ends at a check that holds, so none of them leaves a mechanism half-r
- The layout a node's default root uses beneath it, beyond one directory per assignment.
- Whether a module provider's answer can change without the provider being asked, for example a
provider moving. The rule so far is that it cannot, and moving is re-resolving.
- **How rotation keeps a recipient from being locked out.** Under review: see
[ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), rotation.