From d29d3dfc235ba9decdf698c16574b3969a1128b8 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 12:17:49 +0200 Subject: [PATCH] ADR 0158: a provider with one credential shares it with every consumer, and the vault remakes it for all at once; designs 24 and 13 carry it --- ...redential-shares-it-with-every-consumer.md | 112 ++++++++++++++++++ 02-DECISIONS/README.md | 1 + .../13-credentials-and-their-rotation.md | 4 +- 03-DESIGN/01-to-be/24-the-secrets-vault.md | 19 ++- 4 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md diff --git a/02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md b/02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md new file mode 100644 index 0000000..bc07a24 --- /dev/null +++ b/02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md @@ -0,0 +1,112 @@ +--- +topic: the mesh +status: accepted +date: 2026-10-01 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md +--- + +# 158. A provider with one credential shares it with every consumer, and the vault remakes it for all of them at once + +## Context + +[ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) gave every consumer of a +provision its own credential: the mesh mints one per pair, the provider's own code creates the +login, and rotating one consumer's touches nothing else. That is right for a database, a broker, an +object store — software that can hold many logins. + +The media software on the home server cannot. A download client has one web password; an indexer +has one API key; each of the library managers has one key in its configuration; the media server +holds one token issued elsewhere. There is no login per consumer to create, so +[ADR 0113](0113-the-vault-makes-every-secret.md)'s only remaining form applied: the value is +*accepted*. On 2026-10-01 the home server held forty-seven accepted own secrets and twelve accepted +pair credentials, every one rotatable only by a person changing the software by hand and accepting +the new value, and one pair credential sat *made* and wrong because nobody could accept the real one. +The operator asked for every password in the vault and rotatable, and for a library manager's +definition to receive the download client's credential and address through provisioning like +anything else (filed as the forge's issue 243 on this repository). + +The address half already works: the library manager requires the download client's API provision, +the provider serves scheme, port and user name, and the binding carries them. Only the credential +half had no form. + +## Considered Options + +1. **Keep accepting.** Honest about what the software can do and what the mesh cannot, and it is + the state the home server was in: nothing rotates, a consumer added later needs a person, and an + unknown predecessor password stays unknown for ever. +2. **Put a login per consumer in front of the software.** A proxy that holds the one credential and + issues many. A second service per provider, with its own credential to keep, to make the mesh's + model fit software that does not share it. +3. **Let the provider say its one credential is the credential.** An offer names which of the + provider's own secrets *is* what every consumer receives. The vault keeps one record, sealed to + the provider's machine, every current consumer's machine and the operator, and because it stores + no plaintext it cannot seal an existing value to a later consumer — so it **remakes the value + for all of them at once** whenever the set of consumers changes or a rotation is asked. The + provider takes it the way an own secret is taken ([ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md), + issue 180); consumers read it at start. + +## Decision + +**Option 3.** A provider whose software holds one credential shares that credential, and the mesh +owns its whole lifecycle. + +- **The offer says so.** `{"name": "download-client-api", "credential": {"own": "password"}}` on a + provider's `provides` entry names one of its own secrets as the credential of that provision. The + named own secret must say how it is taken (`taken: at-start` or `taken: applied`); an offer + naming an undeclared or untaken secret is refused at parse. +- **One record, many seals.** The vault keeps one value per (provider assignment, provision). It is + sealed to the provider's machine, to each consumer's machine that currently binds the provision, + and to the operator. Every consumer's binding file carries the provider's one user name and the + secret file carries the shared value; the shape a consumer reads is the pair credential's, so a + consumer's definition does not know whether its credential is shared. +- **Remade for all, together.** When a consumer binds or unbinds, or `secret rotate` is asked on the + provider's own secret, the vault makes a new value and seals it to every current holder in one + act, and the mesh sends every holding machine. The provider restarts on the new value or applies + it at start; each consumer restarts on it. There is no window between two credentials, because + there is one credential; there is the restart, stated as the cost below. +- **An accepted shared value is sealed to everyone the moment it is accepted.** `secret accept` on + the provider's own secret is the one moment the mesh holds the plaintext, and it seals copies for + every current consumer then. It is not remade afterwards ([ADR 0113](0113-the-vault-makes-every-secret.md)): + a consumer that binds later is refused until the value is accepted again, in words that say so. +- **A value the software issues itself stays accepted.** A token the media server obtains from its + vendor cannot be set by the mesh; its provision keeps the accepted form until a module can deliver a + value it did not mint to the vault, which this record does not build. +- **Nothing changes for software that holds many logins.** ADR 0048's form stays the default; this + is the form for an offer that says it has one credential. + +## Consequences + +- The media stack's six providers stop needing a person per consumer. A library manager binding + the download client gets a working credential the mesh made, and an unknown predecessor password + is replaced by one the mesh knows, recoverable with the operator's key. +- **Adding or removing a consumer restarts every consumer of that provision and the provider.** + That is the price of one credential, and it is paid when a definition binds, not at an hour of + nobody's choosing. It is stated in the plan's words when it happens. +- Rotation of a shared credential is [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md)'s + single-party form across several machines: in place, all holders sent together. The staged form + for a backend that takes its credential once is still not built, and a provider whose own secret + says `applied` refuses rotation by name until it is. +- The vault can name who holds a shared value — the copies are the record — so *who has this* stays + a query, as design 13 requires. +- The accepted count on the home server becomes a list that shrinks, provider by provider, as each + one's start applies the file. + +## How this is checked + +| Rule | Checked by | +|---|---| +| An offer may name one of its own secrets as its credential; an undeclared or untaken secret is refused at parse | manifest tests | +| A consumer of a shared provision receives the provider's value as its pair credential, under the provider's one user name | resolver and declaration tests | +| The record is sealed to the provider, every current consumer and the operator; a consumer binding or unbinding remakes it for all | inventory tests against a raised store | +| Rotating the provider's own secret remakes every holder's copy, and an accepted value is sealed to current consumers once and not remade | inventory tests | +| Live: a library manager on the home server binds the download client with a value the mesh made, the client takes it at start, and a rotation through the console reaches both | done by hand after the media catalogue's providers apply the file at start | + +## References + +- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) — extended: the per-consumer form stays the default; this is the form for one credential +- [ADR 0113](0113-the-vault-makes-every-secret.md), [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md) — the accepted form and the single-party rotation this rests on +- [Issue 180](../04-ISSUES/180-a-modules-own-secret-cannot-be-rotated/00-report.md) — the `taken` word and the rotation this reuses +- [Design 24 — The secrets vault](../03-DESIGN/01-to-be/24-the-secrets-vault.md), [Design 13 — Credentials and their rotation](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md) +- The forge's issue 243 on this repository, where the operator's ask and the home server's count were recorded diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 07aacb8..69fc022 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -171,6 +171,7 @@ python3 00-META/checks/index.py fail if stale - **0154** — [The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md) - **0156** — [An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md) - **0157** — [A build says what it does on the bus, as it happens](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md) +- **0158** — [A provider with one credential shares it with every consumer, and the vault remakes it for all of them at once](0158-a-provider-with-one-credential-shares-it-with-every-consumer.md) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/13-credentials-and-their-rotation.md b/03-DESIGN/01-to-be/13-credentials-and-their-rotation.md index bddad63..0c11e22 100644 --- a/03-DESIGN/01-to-be/13-credentials-and-their-rotation.md +++ b/03-DESIGN/01-to-be/13-credentials-and-their-rotation.md @@ -145,5 +145,7 @@ and the operator, and sends the machine, so the module starts again on it. A sec is refused with the word to write, because a credential rotated under software that never reads it again is the fault of issue 179 made deliberately; an applied one is refused until the staged form is built; an accepted one is refused as ADR 0113 says. `rotate` is a verb on the controller's seat with -both shapes, so the console asks for either. *How it is checked:* the tests named in issue 180, and a +both shapes, so the console asks for either. A provider that shares its one credential with every +consumer ([ADR 0158](../../02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md)) +rotates the same way, with every holder's copy remade and every holding machine sent together. *How it is checked:* the tests named in issue 180, and a live rotation through the console of a secret a module reads at start. diff --git a/03-DESIGN/01-to-be/24-the-secrets-vault.md b/03-DESIGN/01-to-be/24-the-secrets-vault.md index 898abe5..1f88d13 100644 --- a/03-DESIGN/01-to-be/24-the-secrets-vault.md +++ b/03-DESIGN/01-to-be/24-the-secrets-vault.md @@ -2,8 +2,9 @@ layer: to-be status: implemented code: [mesh-catalog, mesh-controller, mesh-host] -updated: 2026-09-21 +updated: 2026-10-01 decisions: + - 02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md - 02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md - 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md - 02-DECISIONS/0085-a-secret-is-a-provision.md @@ -127,6 +128,22 @@ credential a provider grants; the export names each entry by the node and module the name they know it by, and says whether it is a module's own secret or a pair credential, so recovery addresses both alike. +### A provider with one credential + +*Decided 2026-10-01 ([ADR 0158](../../02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md)); to be built.* + +Software that holds one credential — a download client's web password, an indexer's one API key — +cannot give each consumer a login, so ADR 0048's form does not fit it and its values were accepted +by hand. An offer may now say `"credential": {"own": ""}`: the provider's own secret *is* the +credential every consumer of that provision receives, in the shape of an ordinary pair credential, +under the provider's one user name. The vault keeps one value per provider assignment and provision, +sealed to the provider's machine, each consumer's machine and the operator; because it holds no +plaintext it remakes the value for every holder at once when a consumer binds or unbinds or a +rotation is asked, and the mesh sends every holding machine together. The provider takes it as it +says it takes its own secret (`taken`, issue 180); consumers read it at start. An accepted value is +sealed to the consumers of the moment and not remade; a consumer that binds later waits for the next +acceptance. *How it is checked:* the rows of ADR 0158's table, once built. + ## Beyond generate and hold Owning a secret means owning more than its creation. The mesh being migrated onto has a working -- 2.54.0