From 43f63ed41c4f99a88ca874cdbe01abc5db1db8ed Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 26 Sep 2026 00:22:21 +0200 Subject: [PATCH] ADR 0114: a two-party credential rotates over two logins Graduates research 016. Retiring a login is separated from removing a consumer, which closes a data-loss path in five providers; single-party secrets rotate in place; the number of parties decides, not the provider. --- .../00-overview.md | 5 +- .../0113-the-vault-makes-every-secret.md | 16 +- ...ared-credential-rotates-over-two-logins.md | 198 ++++++++++++++++++ 02-DECISIONS/README.md | 1 + .../27-a-module-requires-the-mesh-resolves.md | 34 +-- 5 files changed, 234 insertions(+), 20 deletions(-) create mode 100644 02-DECISIONS/0114-a-shared-credential-rotates-over-two-logins.md diff --git a/01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md b/01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md index 83f184d..4c28849 100644 --- a/01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md +++ b/01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md @@ -1,5 +1,8 @@ --- -status: active +status: graduated +became: + - 02-DECISIONS/0114-a-shared-credential-rotates-over-two-logins.md + - 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md initiated: 2026-09-26 touches: - 02-DECISIONS/0113-the-vault-makes-every-secret.md diff --git a/02-DECISIONS/0113-the-vault-makes-every-secret.md b/02-DECISIONS/0113-the-vault-makes-every-secret.md index cc70c48..36b60a6 100644 --- a/02-DECISIONS/0113-the-vault-makes-every-secret.md +++ b/02-DECISIONS/0113-the-vault-makes-every-secret.md @@ -164,10 +164,12 @@ the change using the old value. Where no provisioner can make it, the requiremen 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 not decided here.** Three mechanisms were measured against every -provider's code in [research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md): -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: +**How old and new change over is decided in [ADR +0114](0114-a-shared-credential-rotates-over-two-logins.md).** Three mechanisms were measured against +every provider's code in [research +016](../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md): 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; @@ -178,8 +180,8 @@ resource. Its findings bound the choice: - every backend can give two logins the same rights over one resource, once the adapter separates the resource from the login. -The mechanism is decided in its own record, on those facts. Until then rotation stays as the -controller implements it, in place, with its window stated. +On those facts, 0114 rotates a credential two parties hold over two logins, rotates one a single +party holds in place, and separates retiring a login from removing a consumer. ## What this changes in earlier records @@ -215,7 +217,7 @@ On acceptance, each of these is superseded or amended by this record, not edited - 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 decided with the mechanism ([research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/03-the-options.md)). + rotation is [ADR 0114](0114-a-shared-credential-rotates-over-two-logins.md)'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 diff --git a/02-DECISIONS/0114-a-shared-credential-rotates-over-two-logins.md b/02-DECISIONS/0114-a-shared-credential-rotates-over-two-logins.md new file mode 100644 index 0000000..29c1b49 --- /dev/null +++ b/02-DECISIONS/0114-a-shared-credential-rotates-over-two-logins.md @@ -0,0 +1,198 @@ +--- +topic: what runs on it +status: proposed +date: 2026-09-26 +deciders: jochen +reconstructed: false +extends: 0113-the-vault-makes-every-secret.md +--- + +# 114. A credential two parties hold rotates over two logins; one a single party holds rotates in place; and retiring a login never removes what it reached + +## Context + +[ADR 0113](0113-the-vault-makes-every-secret.md) decides who asks for a rotation (an operator, or the +vault's policy) and who makes the new value (the vault). It leaves open how old and new change over. +[Research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md) read every provider +in the catalogue against its code: + +- **all eight credential providers re-apply a password in place**, on the same login, every time they + run. The controller's `rotate` command relies on that, and states the window it leaves: between the + provider applying the new value and the consumer restarting with it, the consumer cannot authenticate; +- **seven of eight name the consumer's resource after its login**: a database, a bucket, a virtual host, + a key prefix, a topic prefix. Only the forge's npm registry keeps them apart, because an organisation + owns the packages; +- **five of eight destroy the resource when they remove the login**: postgres, mssql, mongodb, minio and + lavinmq. In today's adapters, *retire a login* and *delete the consumer's data* are one call; +- **only one backend holds two passwords on one login**, redis. A second, the forge, holds several + tokens beside one password; +- **every backend can give two logins the same rights over one resource**: a group role, a database + role, a shared policy, shared permissions, a shared role, a shared key prefix, a shared team. No + adapter does it today; +- **administrative credentials have one party and a fixed name.** The provider module is both the one + that applies the value and the only one that reads it. Three backends take it only at first + initialisation, so it can only be changed through a command run with the old value; +- **no module watches a secret.** Every reader reads at start, and the host already recreates a + container when a file it read at creation changes + ([issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md)). + +A first draft of 0113 chose to overlap old and new "through the adapter's existing create and +remove". In five providers, that remove deletes the consumer's data. The mechanism has to be chosen on +what the providers do, and the danger has to be closed whichever mechanism is chosen. + +## Considered Options + +**1. In place for everything, as today.** Works with every provider unchanged. Rejected for credentials +two parties hold. The window cannot be closed, only shortened, and the two ends are on different +machines with nothing ordering them. A reader the mesh cannot reach, but which still reaches its +provider, is locked out until the mesh reaches it again. + +**2. Two secrets on one login.** The applier adds the new password beside the old one. Rejected. It +works for one provider out of eight. Using it where it exists and something else elsewhere is a +mechanism per provider, which is what the mesh is trying to stop having. + +**3. Two logins over one resource for everything.** Rejected for credentials a single party holds. +The applier and the reader are the same module, so there is no second party to keep working while the +other moves. A fixed administrative name has no second name to alternate with. + +**4. Split by the secret, not by the provider.** A credential two parties hold rotates over two logins; +one a single party holds rotates in place; and retiring a login is separated from removing a consumer +before either is used. Chosen. + +## Decision + +### Retiring a login never removes what it reached + +**A provider's adapter keeps two things apart that today are one:** the consumer's *resource* (its +database, bucket, virtual host, key or topic prefix) and the *login* that reaches it. They get separate +operations: + +- **ensure the resource**, named after the consumer; +- **ensure a login** with a value, holding the consumer's rights over its resource; +- **retire a login**, which removes the login and nothing else; +- **remove the consumer**, which is what removes the resource. It is run only when the consumer is + unassigned, as today, and never by a rotation. Whether the resource's data is kept beyond that stays + [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)'s. + +**The resource is named after the consumer, not after a login.** A consumer's identity is derived from +its assignment ([ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md)), and today its login +is that same string. So **no existing resource is renamed**. The login every consumer holds today +becomes the first of its two, under the name it already has. + +### A credential two parties hold rotates over two logins + +This covers a credential between a consumer and a provider, and a bus account, which the broker's +provisioner applies and its module reads. **Each consumer has two logins, derived by the mesh**: its +identity, and its identity with a short fixed suffix. It uses one at a time, and both hold the same +rights over the one resource. + +1. **The vault makes the new value.** +2. **Each applier ensures the unused login with it**, with the consumer's rights, and leaves the login + in use untouched. It verifies that the new login authenticates and the old one still does, and + confirms. It repeats the confirmation on every reconcile pass until the vault acknowledges it, so a + lost message costs one pass. +3. **Only then are the readers given the new login and value, together.** The host recreates each + reader, because a file it read at creation changed. +4. **Each reader confirms** by being recreated 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 retires it, the + login and nothing else, and verifies that it no longer authenticates. + +`status` shows a rotation as waiting on whichever applier or reader has not moved, and it is not done +until the old login is gone. A reader that cannot be reached keeps working on the old login until it +can, and the rotation waits for it. That wait is shown, never hidden. + +**Where a backend identifies a connection separately from a login, the two are kept apart.** An MQTT +client identifier must be unique per connection, so the mosquitto adapter derives the connection's +identifier from the consumer and the login in use, and two logins never collide. + +### A credential a single party holds rotates in place + +This covers a provider's administrative credential and a module's own secret, which only that module +reads. **The vault makes the new value, and the one party takes it:** + +- **applied**: the party's provisioner changes it using the old value, then confirms. This is the only + form for a backend that takes its administrative credential only at first initialisation, where a + restart would change nothing; +- **read at start**: the host recreates the party. + +There is no window between two parties, because there is only one party. Where neither form can change +the value, the requirement is marked not rotatable by the mesh, and a rotation is refused, saying why +([ADR 0113](0113-the-vault-makes-every-secret.md)). + +### One rule decides which + +**The number of parties that hold the credential decides, never the provider.** A requirement whose +secret has an applier and a reader in different modules rotates over two logins. A secret held by one +module rotates in place. The resolver knows which from the requirement's recipients, so no definition +declares it. + +### Until an adapter can + +**An adapter that cannot yet ensure a second login says so**, and the credentials it applies rotate in +place, as today, with the window stated when the rotation is asked for. That is a migration state, not +a second mechanism. It is listed by a check, and the list shrinks to empty. Separating *retire a +login* from *remove the consumer* comes first in every adapter, because it closes a data-loss path that +exists today, whatever rotation does. + +## What this changes in earlier records + +On acceptance, each of these is amended by this record, not edited: + +- [ADR 0113](0113-the-vault-makes-every-secret.md): the changeover it left open is decided here. +- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md): a consumer's identity leaves room + for the second login's suffix within the tightest backend it reaches, and both logins are checked + against it. +- [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md): a module's broker + account is two logins with the same permissions over the same queue, one in use at a time. Its scoping + is unchanged. +- [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md): rotation of a two-party + credential is no longer all-or-nothing with a window. It overlaps, with each step confirmed. + Single-party rotation keeps the form to-be 13 describes. + +## Consequences + +- **Every credential provider's adapter changes**, in two steps. The first separates *retire a login* + from *remove the consumer*, and names the resource after the consumer, which is the name it already + has. The second ensures a second login with the same rights. That is seven adapters for the second + step, since the forge's already holds its packages apart from the user. +- **The SDK's provider harness** carries the alternation, the verification of both logins, and the + repeated confirmation, so no adapter implements them. Its record of what was applied has to survive a + restart of the provisioner mid-rotation. Today it is kept in memory. +- **No consumer module changes.** It reads one login and a value at start, as today, and is recreated + by the host when they change. +- **The derived identity is two characters tighter** in the tightest backend, a minio access key of 20 + characters. +- **What got harder:** a provider briefly holds two logins per consumer. A rotation of a two-party + credential lasts until its slowest reader moves, so an unreachable reader keeps the old login valid + until it is reached. And an adapter has four operations where it had two. + +## How it is checked + +| Rule | Checked by | +|---|---| +| Retiring a login never removes a resource | A provider test per credential provider: retiring one of a consumer's logins leaves its resource and data intact, reachable through the other. | +| Removing a consumer is not a rotation | A harness test: no rotation step calls remove; remove runs only when a contribution goes. | +| No existing resource is renamed | A provider test: a consumer created before the change keeps its resource, and its existing login becomes the first of its two. | +| Both logins hold the same rights | A provider test per credential provider: data written under one login is read and changed under the other. | +| Readers move only after the applier confirms | A rotation test: readers receive nothing until both logins authenticate at every applier. | +| The old login is retired only after every reader confirms | A rotation test with one reader's node unreachable: it keeps authenticating with the old login, the rotation shows waiting on it, and it completes when the reader returns and confirms. | +| A lost confirmation costs one pass | A harness test dropping the first confirmation: the next pass repeats it. | +| A restarted provisioner resumes a rotation | A harness test restarting the provisioner between steps: it resumes from the step it reached. | +| A single-party secret rotates in place | A vault test: a provider's administrative credential is applied by its own provisioner with the old value; a module's own secret recreates the module; neither has a second login. | +| The number of parties decides | A resolution test: a secret with an applier and a reader in different modules is marked for two logins, and one held by one module for in place, with nothing declared. | +| Both logins fit the tightest backend | A controller test: both derived logins for the longest node and module names fit the limit ADR 0049 sets. | +| A connection identifier never collides | A mosquitto provider test: two connections under a consumer's two logins are both accepted. | +| Adapters still in place are listed | A catalogue test lists every credential provider that cannot yet ensure a second login. The list shrinks to empty, and a rotation of their credentials states its window. | + +## References + +- [Research 016](../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md): the survey this + rests on, provider by provider +- [ADR 0113](0113-the-vault-makes-every-secret.md): who asks and who makes +- [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md), [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md), + [ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md): identity, bus accounts, and data outliving + its declaration +- [To-be 13](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md): rotation as implemented +- [Issue 103](../04-ISSUES/103-a-container-is-not-recreated-when-a-file-it-reads-changes/00-report.md): + why a reader's restart can be derived diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 70b2bcb..8bb0aad 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -159,6 +159,7 @@ python3 00-META/checks/index.py fail if stale - **0110** — [A seat is held by one assignment, from a closed set, and it may deliver a provision](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) *(proposed)* - **0112** — [A module definition names no node, no mesh and no path: everything it needs is a requirement the mesh resolves](0112-a-module-definition-names-no-node-mesh-or-path.md) *(proposed)* - **0113** — [The vault makes every shared secret, a provider makes resources and data, and the mesh carries both](0113-the-vault-makes-every-secret.md) *(proposed)* +- **0114** — [A credential two parties hold rotates over two logins; one a single party holds rotates in place; and retiring a login never removes what it reached](0114-a-shared-credential-rotates-over-two-logins.md) *(proposed)* ### How it is built 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 10d1ac9..ed64d18 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 @@ -6,6 +6,7 @@ updated: 2026-09-26 decisions: - 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md - 02-DECISIONS/0113-the-vault-makes-every-secret.md + - 02-DECISIONS/0114-a-shared-credential-rotates-over-two-logins.md - 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md - 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md - 02-DECISIONS/0084-which-provider-serves-a-consumer.md @@ -248,12 +249,23 @@ process, or a file in a mounted directory. A secret a service takes only at firs applied by its provisioner or marked not rotatable by the mesh, and a rotation of it is refused rather than reported done. -**How old and new change over is not settled.** Until it is, rotation stays as the controller does it -today: in place, both ends sent in one push, with a stated window in which a consumer cannot -authenticate. [Research 016](../../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md) -measured three mechanisms against every provider. One constraint holds whichever is chosen: **retiring a -credential must never remove a consumer's resource.** Today's adapters remove both in one call, and in -five providers that deletes the consumer's data. +**How old and new change over depends on how many parties hold the credential** +([ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-logins.md), on +[research 016](../../01-RESEARCH/016-how-a-credential-can-be-rotated/00-overview.md)): + +- **Two parties**, a consumer and its provider, or a module and the broker: each consumer has two logins + derived by the mesh, both with its rights over one resource, named after the consumer. The vault makes + the new value; each applier ensures the unused login with it and confirms both authenticate; only then + are readers given it and recreated; once every reader has confirmed, the old login is retired. Nobody + is left without a credential that works, and `status` shows who a rotation waits on. +- **One party**, a provider's administrative credential or a module's own secret: in place. Its own + provisioner applies it with the old value, or the host recreates it. + +**Retiring a login never removes what it reached.** An adapter keeps *retire a login* and *remove the +consumer* apart. Only unassigning removes the resource, and never a rotation. Today the two are one +call, and in five providers it deletes the consumer's data, so this separation comes first. An adapter +that cannot yet ensure a second login rotates in place, with its window stated, and is listed until it +can. ## Refusing @@ -302,8 +314,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 in the host already. *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, the - vault making the value, the consumer recreated by derivation, and its data intact. + genesis, a lab consumer of analytics receives its site id, and a database credential rotates over its two + logins, the consumer recreated by derivation, never without a working login, and its data intact. 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 @@ -329,15 +341,13 @@ 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. | -| Restarts are derived, and rotation keeps data | The tests of [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md): an applied secret restarts nothing, one read at start recreates its reader without a declared restart, and rotating a consumer's credential leaves its resource and data intact. | +| Restarts are derived | The tests of [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md): an applied secret restarts nothing, and one read at start recreates its reader without a declared restart. | +| A two-party credential rotates over two logins | The rotation tests of [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-logins.md): retiring a login leaves the resource intact; readers move only after the applier confirms; an unreachable reader keeps its old login until it returns; a single-party secret rotates in place. | | 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. | ## Not settled here -- How old and new credentials change over on rotation: in place, as today, or two logins over one - resource, as [research 016](../../01-RESEARCH/016-how-a-credential-can-be-rotated/03-the-options.md) - recommends for credentials with two parties. It is decided in its own record. - The exact spelling of the one form. It must name a requirement and a field and nothing else. - The layout a node's default root uses beneath it, beyond one directory per assignment.