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