diff --git a/02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md b/02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md index 80e54e7..a03831a 100644 --- a/02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md +++ b/02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md @@ -163,6 +163,14 @@ value, the requirement is marked not rotatable by the mesh, and a rotation is re **The number of parties decides, never the provider.** The resolver knows it from the requirement's recipients, leaving out the vault's custody copy, so no definition declares it. +> **Progressive insight — 2026-10-01.** The number of parties is the resolver's to know; *which form* +> a single party's credential takes is not, and cannot be: whether a module reads its secret when it +> starts or applies it once to a backend is a fact about the software, visible nowhere in the graph. +> So the definition declares that half — `taken: at-start` or `taken: applied` on an own secret — and +> a secret that declares neither is not rotated, refused with the word to write (issue 180). The +> read-at-start form is built; the staged form for an applied credential is not. The decision stands; +> the sentence above was one fact short. + ### Until an adapter can **An adapter that cannot yet ensure a second credential says so.** The two-party credentials it applies 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 b5db27e..bddad63 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 @@ -5,7 +5,7 @@ code: - mesh-controller internal/inventory/secrets.go - mesh-controller cmd/mesh-controller/rotate.go - mesh-controller examples/postgres-provisioner -updated: 2026-09-21 +updated: 2026-10-01 decisions: - 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md - 02-DECISIONS/0009-modules-and-the-graph.md @@ -136,3 +136,14 @@ rotates, and is queried for who holds it, through exactly the machinery describe The rotation a module's own secret lacks is not a second mechanism; it is this one, pointed at a secret the vault provides. What this page proves for a database password holds, by construction, for a secret from the vault. + +*Built 2026-10-01, the read-at-start half ([issue 180](../../04-ISSUES/180-a-modules-own-secret-cannot-be-rotated/00-report.md), +[ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md)).* An own secret +says how the module takes it — `taken: at-start` or `taken: applied` on its entry — and the mesh +rotates only the first: `secret rotate ` makes it anew, seals it to the machine +and the operator, and sends the machine, so the module starts again on it. A secret that says neither +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 +live rotation through the console of a secret a module reads at start. diff --git a/04-ISSUES/180-a-modules-own-secret-cannot-be-rotated/00-report.md b/04-ISSUES/180-a-modules-own-secret-cannot-be-rotated/00-report.md new file mode 100644 index 0000000..07ef9fa --- /dev/null +++ b/04-ISSUES/180-a-modules-own-secret-cannot-be-rotated/00-report.md @@ -0,0 +1,65 @@ +--- +status: resolved +opened: 2026-10-01 +located-in: [mesh-controller cmd/mesh-controller/secret.go (accept and now rotate), mesh-controller internal/inventory/secrets.go (a module's own secret), mesh-controller internal/catalogue/manifest.go (how an own secret is taken), the controller's seat (rotate was not a verb)] +fixed-by: mesh-controller PR 183 (`secret rotate`, the `taken` word on an own secret, `rotate` on the controller's seat with two shapes); the staged form for an applied secret stays open below +amended-design: [03-DESIGN/01-to-be/13-credentials-and-their-rotation.md, 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md] +--- + +# 180 — A module's own secret cannot be rotated, and nothing rotates from the console + +## What was observed + +Filed first in the forge's tracker on this repository (its issue 231, 2026-09-30), after a module's +API token was printed by accident on the home server and the only way to change it was to generate +a value by hand and `secret accept` it. The report said there was no rotation at all. That was half +right: `rotate ` has existed for a pair credential since design 13, pushing both ends +together; what did not exist was any rotation of a **module's own secret** — a token, an application +secret, an administrator — and any way to ask for either through the console +([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)). + +## Why this is here + +[ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md) decided how a +single party's credential rotates: in place, in one of two forms. **Read at start** — the vault +delivers the new value as current and the party is started again. **Applied** — the party's own +code applies the value to a backend that takes it once, so the new value must be staged beside the +current one until the party confirms it. Neither form was built, and nothing said which form a given +secret needed. That last gap is the dangerous one: [issue 179](../179-an-adopted-identity-providers-admin-never-took-the-minted-secret/00-report.md) +is what a value looks like when the mesh believes it was taken and the software never read it. A +rotation that made that happen on purpose would be worse than no rotation. + +## Resolved, 2026-10-01 — the read-at-start form, and the verb + +**An own secret says how it is taken.** In a definition, `"own-secrets": {"api-token": {"path": …, +"taken": "at-start"}}` says the module reads the file when it starts; `"taken": "applied"` says its +own code applies the value to a backend; a path alone says neither. A definition that says neither +is not rotated by the mesh, and the refusal names the word to write. + +**`secret rotate `** makes the secret anew the way the first mint did, seals it +to the machine and to the operator, and sends the machine, so the module starts again on the new +value, under the same `restart-on` that any changed file triggers. Said in the log with who asked and +when, never the value. An applied secret is refused by name, until the staged form exists. A value +given to the mesh rather than made by it is refused as [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md) +says, with the way out: change it where it lives, then accept the new value. + +**`rotate` is a verb on the controller's seat** ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)), +with the two shapes the mesh has: a pair credential by provision and consuming machine, or an own +secret by machine, module and name. The console can ask for either. + +Nothing in the catalogue says `taken` yet: the word ships one release ahead of its first use, and the +first definitions to say it follow once this controller runs. + +*How it is checked:* the manifest form and its refusals; the rotation against a raised store — +rotates a secret taken at start, refuses an applied one, an undeclared one, an accepted one and an +unknown name, each in its own words; the verb's two shapes; and, live, a secret rotated through the +console on a module that reads it at start, the module restarted, and the module working. + +## Open — the applied form + +The staged rotation ADR 0114 decided for an applied secret is not built: the vault delivering the +new value beside the current one, the module's own code switching the backend and confirming, and +only then the new value current. It needs a word in the module's protocol for *confirm*, and it is +the form the identity provider's administrator and every database's superuser need. The request's +second half — re-issuing a pair credential through the provider's own code rather than by re-minting +and pushing — is the same shape from the provider's side, and sits with it.