Files
hq/04-ISSUES/068-secrets-have-no-owning-module/00-report.md
T
jschoubben 36d9b38a0d An issue is open, diagnosing, located, resolved or wontfix — nothing else
The playbook, the README and the status skill knew five statuses; the cycle check
knew a sixth, 'fixed', and not 'wontfix'. Eleven issues sat in the sixth for weeks
with their fixes shipped, one step short of closed. They are resolved; the check
refuses the word from now on and accepts the one the playbook allows.
2026-09-21 17:38:17 +02:00

5.3 KiB

status, opened, located-in, fixed-by, amended-design
status opened located-in fixed-by amended-design
resolved 2026-09-20
hq 03-DESIGN/01-to-be/24-the-secrets-vault.md
graduated — hq fc4ab37 (ADR 0085, to-be design 24) 03-DESIGN/01-to-be/24-the-secrets-vault.md

Secrets have no owning module

Symptom, as observed

Every other piece of shared infrastructure in the mesh has been made an ordinary module that claims a seat: the store seat and the broker seat are each filled by a module that runs its own code, provisions for its consumers, and is built and delivered like any other (ADR 0047, ADR 0048, issue 051). Secrets are the exception. There is no module that owns a secret.

Secret handling is instead smeared across three built-in parts of the controller and node runtime:

  • the controller mints — one password per consumer↔provider pair, sealed to both node keys (ADR 0048);
  • the mesh database holds — a secret is a database record, never a file in the repository (as-is design, 06-configuration-and-secrets.md);
  • the synchroniser injects — a generated secret is written into a node's generated env file and kept stable across regenerations.

Nothing is the owner of "a secret" the way the store module is the owner of "a database." The consequences are the gaps we already have written down separately:

  • Generated local secrets exist but cannot be rotated by a mesh operation. The as-is design records the weakness verbatim — "Rotation is not a mesh operation… there is no mechanism that rotates one and informs everything holding it". A rotate <provision> command has since been added and proven for provisioned provider↔consumer credentials, but it reaches only those pairs; a secret a module generates for its own fully-local use (the password of a version-pinned embedded database, for instance — see issue 067) is minted by the mesh and then has no operation that can remake it.
  • Three secret paths, no single provider behind them. Provisioned credentials (ADR 0048), generated local secrets (a manifest's generated-secret env var), and operator-delivered secrets (secret accept, sealed to a node) are three separate mechanisms. Nothing unifies "the mesh has a secret and is responsible for its whole life."

Why it matters beyond this instance

  • It is the same de-specialisation the mesh already committed to, left half done. Making the store and broker seats ordinary modules was a deliberate decision precisely so infrastructure would not be a privileged property of the controller that no module owns, cannot be reasoned about as a module, and cannot be built or replaced like one. Secret-minting is still exactly that privileged controller property. Either the store/broker decision was right and this should follow it, or it was wrong — but the mesh should not be half one and half the other with no record of why.
  • Rotation, backup, audit and break-glass have nowhere to live. These are provider responsibilities everywhere else (the store provisioner rotates a DB credential; a provider is where a resource's lifecycle lives). With no secrets provider, each of these is either absent or a one-off in the controller. The mesh being migrated onto has a working secrets subsystem to learn from — the source mesh exposes locate / backup / verify / break-glass / preflight / generate operations — none of which has an owner on the nox side.
  • It blocks the "assume every old secret leaked" step of the migration. The cutover plan ends with a full rotation of every credential on the assumption the old ones are compromised. Today that step is only expressible for provisioned pairs; app-internal and generated-local secrets must be rotated by hand, which is the exact operation the design says has taken services down when done wrong.

Open questions

  • Is the vault a module that claims a seat (like store and broker), a provider that offers a secret provision that other modules require, or both — a seat whose claimant is also the provider of secrets to everyone else?
  • Does a consumer request a secret the way it requests a database (requires: secret, the vault mints and delivers it), so that a fully-local embedded service's password is a provisioned secret rather than a magic generated env var?
  • Does the vault subsume the three existing paths (provisioned credentials, generated local secrets, operator-delivered secrets), or sit beside them owning only rotation/backup/audit? Subsuming is cleaner but is a migration of every provider that mints today.
  • Is the vault node-scoped like every other provider (per issue 067) — each node's own vault — or is there a case for a single mesh vault, and if so how does that survive the same objections that made the store node-scoped?
  • What does rotation of a secret mean when the holder is a local-only service the mesh cannot reach as a consumer — does the vault restart the holder, or hand the new value to the holding module's own runtime to apply?
  • How does break-glass work — recovering a secret when the normal sealed path is unavailable — without reintroducing a key some single place holds, which ADR 0048 went out of its way to avoid?