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
This commit is contained in:
@@ -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 <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?
|
||||
Reference in New Issue
Block a user