Issue 180: a module's own secret rotates when it is read at start; the applied form stays open (controller PR 183); design 13 and ADR 0114 carry the word

This commit is contained in:
2026-10-01 11:43:23 +02:00
parent 027e5b8d73
commit 48a620249b
3 changed files with 85 additions and 1 deletions
@@ -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 **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. 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 ### Until an adapter can
**An adapter that cannot yet ensure a second credential says so.** The two-party credentials it applies **An adapter that cannot yet ensure a second credential says so.** The two-party credentials it applies
@@ -5,7 +5,7 @@ code:
- mesh-controller internal/inventory/secrets.go - mesh-controller internal/inventory/secrets.go
- mesh-controller cmd/mesh-controller/rotate.go - mesh-controller cmd/mesh-controller/rotate.go
- mesh-controller examples/postgres-provisioner - mesh-controller examples/postgres-provisioner
updated: 2026-09-21 updated: 2026-10-01
decisions: decisions:
- 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md - 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
- 02-DECISIONS/0009-modules-and-the-graph.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 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, secret the vault provides. What this page proves for a database password holds, by construction,
for a secret from the vault. 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 <node> <module> <name>` 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.
@@ -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 <provision>` 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 <node> <module> <name>`** 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.