From 805df3f81ed9ed3ab7464473593269e30c7bb594 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 23:50:20 +0200 Subject: [PATCH] ADR 0113 and to-be 27: rotation overlaps old and new credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../0113-the-vault-makes-every-secret.md | 94 ++++++++++++------- .../27-a-module-requires-the-mesh-resolves.md | 52 +++++----- 2 files changed, 88 insertions(+), 58 deletions(-) diff --git a/02-DECISIONS/0113-the-vault-makes-every-secret.md b/02-DECISIONS/0113-the-vault-makes-every-secret.md index a5b5769..465f8c6 100644 --- a/02-DECISIONS/0113-the-vault-makes-every-secret.md +++ b/02-DECISIONS/0113-the-vault-makes-every-secret.md @@ -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 diff --git a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md index ca648af..4e7473a 100644 --- a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md +++ b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md @@ -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.