Files
hq/04-ISSUES/068-secrets-have-no-owning-module/00-report.md
T
jschoubben d66579c8f6 Issue 068 — secrets have no owning module
Store and broker seats were made ordinary modules; secret-minting is still a
privileged property of the controller that no module owns. Propose the vault
become a module that provides a secret provision (generate/hold/rotate/backup/
audit), node-scoped like every other provider (issue 067), subsuming the three
secret paths and giving local-secret rotation a home.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-20 21:24:42 +02:00

5.2 KiB

status, opened, located-in, fixed-by, amended-design
status opened located-in fixed-by amended-design
open 2026-09-20

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?