diff --git a/04-ISSUES/068-secrets-have-no-owning-module/00-report.md b/04-ISSUES/068-secrets-have-no-owning-module/00-report.md new file mode 100644 index 0000000..bc2fdbb --- /dev/null +++ b/04-ISSUES/068-secrets-have-no-owning-module/00-report.md @@ -0,0 +1,90 @@ +--- +status: open +opened: 2026-09-20 +located-in: [] +fixed-by: +amended-design: +--- + +# 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](../../02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md), +[ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md), +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 ` 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?