Merge pull request 'The secrets vault: handoff, the amended decision, issues 069–071, and the reconciliation of open issues' (#58) from feat/secrets-vault into main

This commit was merged in pull request #58.
This commit is contained in:
2026-09-21 10:03:28 +02:00
26 changed files with 328 additions and 50 deletions
Binary file not shown.
+12
View File
@@ -28,6 +28,16 @@ One feature is **one branch name**, **one worktree per repo**, **one MR per repo
git worktree add .work/<slug>/<repo> -b feat/<slug> origin/main
```
Parallel features never collide, and no shared checkout is edited.
**Add the untouched siblings the lab reads.** The lab beds find the other repositories by
sibling path from the lab checkout (`../mesh-tools/module.json`, `../mesh-sdk`, …), the way
the main layout has them. A `.work/<slug>/` directory holding only the touched repos fails a
bed at once with `no manifest for mesh-tools at …/.work/<slug>/mesh-tools/module.json`, after
genesis has already passed. Give the directory those repos as **detached worktrees on `main`**
— never symlinks:
```
git worktree add --detach .work/<slug>/mesh-tools main
```
3. **Commit as you go — locally.** Increments land on the one branch. Nothing is pushed and no
MR is opened mid-feature.
4. **Finish, then publish.** When the whole feature is done — every repo, tests green — push
@@ -52,6 +62,8 @@ The end state is visible, and its absence is the smell:
- After a feature merges, `git branch -r | grep feat/<slug>` and `git worktree list` return
nothing for it. A surviving branch or worktree means step 6 was skipped.
- A bed run from `.work/<slug>/mesh-lab` that fails naming a `.work/<slug>/<repo>/…` path it
cannot find is a missing sibling worktree, not a mesh fault.
- More than one open MR in a repo that share no feature name, or a stack of MRs none of which is
merged, is the failure this playbook exists to prevent — stop and consolidate before opening
more.
@@ -2,6 +2,7 @@
topic: what runs on it
status: accepted
date: 2026-09-20
amended: 2026-09-20
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0031-the-control-plane-authenticates-nobody.md
@@ -93,6 +94,51 @@ the vault offers recovery without it is left to the design as an open question.
today bakes a password into its own composition must instead require it from the vault — a
migration taken module by module, not a flag day.
## Amendment — 2026-09-20, before anything shipped
Recorded on the record itself rather than as a supersession, by its decider, on the day it was
accepted and before any code was merged against the sentences that change. The original text above
is left as written; this section says what it got wrong and what stands instead.
**What it got wrong.** The decision treated the vault as a provider like the store — optional, and
one per node — and left the mesh's own root secrets outside it: the store's superuser, the broker's
administrator, the controller's contexts, sealed to a node key and nothing else. Those are the
secrets with no rotation and no recovery, and they are the ones a vault exists for. At genesis they
are not even secret: the foundation raises its store and broker with fixed, well-known credentials
and carries those into the mesh. Leaving that floor in place gave module secrets an owner and the
root secrets none.
**What stands instead.**
- **The vault is a foundation module.** It is installed at genesis as part of the foundation
([ADR 0078](0078-the-store-and-broker-are-modules.md) is the precedent: a foundation piece is
still an ordinary module), not assigned later by a mesh that happens to want one. *"A mesh that
wants no vault runs none"* is withdrawn. A mesh has root secrets, so a mesh has a vault.
- **One per mesh, on the control-node.** *"The vault is node-scoped like every provider"* is
withdrawn. Node scoping ([ADR 0084](0084-which-provider-serves-a-consumer.md)) exists because a
store holds data a consumer is coupled to; the vault holds nothing a consumer is coupled to, and a
second one would be a second place to lose. Consumers on other nodes reach it as they reach
identity.
- **The vault holds the mesh's root secrets under an operator-held key.** The controller mints and
delivers exactly as before; in addition, every secret a module holds for itself is sealed a second
time, to an **operator sealing key** whose private half never enters the mesh. The vault keeps
those operator-sealed copies on its own disk, outside the store, and can hand them out — they are
ciphertext to everything but the operator. This is the break-glass path the original text left
open, and it does **not** reintroduce a key one place holds: the mesh holds blobs it cannot open,
and the operator holds a key with nothing to open until given a blob. Recovery needs both.
- **Genesis mints real root secrets and seals them to the operator key first**, so the fixed
credentials the foundation is raised with are replaced before the mesh is handed over.
**Unchanged.** A module's own secret is a `secret` provision the controller mints and the vault
records; the provisioned-pair path of [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)
is untouched; the vault stores no plaintext, ever.
**What it costs.** An operator key is a thing a person must keep, and a mesh whose operator key is
lost has root secrets that can be rotated but not recovered — the same standing as today, stated.
Sealing every own secret twice is a column and a call. Genesis grows a step. A module's
vault-provided secret (a pair credential) is not yet sealed to the operator key; that is the next
increment, not this one.
## References
- [ADR 0031](0031-the-control-plane-authenticates-nobody.md) — identity is a module; this is the
+8 -1
View File
@@ -8,7 +8,7 @@ code:
- mesh-catalog modules/postgres
- mesh-catalog modules/lavinmq
- mesh-lab test/integration/mesh.test.ts (a bare machine becomes a mesh)
updated: 2026-09-17
updated: 2026-09-21
decisions:
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
- 02-DECISIONS/0078-the-store-and-broker-are-modules.md
@@ -246,6 +246,13 @@ host's vocabulary grows by one shape rather than by one resource type per founda
the module that provides one.
- **Whether one host can raise all three.** The claim under stage 2 of
[the node host](05-the-node-host.md), never proved. If it is false, the tier boundary moves.
- ~~**The vault as the fourth piece.**~~ **Closed 2026-09-21** by
[ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md) as amended: genesis makes the
operator key before anything is minted, raises the store and broker with credentials it made
rather than the template's, adopts both as modules with those credentials, installs
`mesh-vault` beside them, and writes the operator-sealed export next to the key
([24](24-the-secrets-vault.md), [issue 071](../../04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md)).
Proven by the one-node genesis bed's root-secrets step.
- **How the foundation is updated once a mesh exists.** Pinned by hand at bootstrap; afterwards
the controller could deliver it like anything else, and nothing says whether it does.
@@ -5,7 +5,7 @@ code:
- mesh-host internal/bootstrap
- mesh-host cmd/mesh-bootstrap
- mesh-lab test/integration/one-node-mesh.test.ts
updated: 2026-09-15
updated: 2026-09-21
decisions:
- 02-DECISIONS/0067-genesis-is-a-pivot.md
- 02-DECISIONS/0073-the-installer-carries-a-builder.md
@@ -118,6 +118,17 @@ told anything.
this is the lab being honest rather than the mesh being broken. What is missing is the step that
puts the shipped unit on the machine.
**The foundation's credentials are the installer's, not the template's.** The template still
carries a fixed store password and the broker image's default administrator, because it is applied
raw by beds that raise no mesh; the installer replaces both with values it makes once and keeps at
the paths the store and broker modules declare, rewrites the produced bundle to use them, and
writes that bundle at 0600. After enrolment and before the first secret is accepted it makes the
operator key beside the bundle and gives the mesh its public half; the run ends with the
operator-sealed export beside the key ([24](24-the-secrets-vault.md),
[issue 071](../../04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md)).
What is not yet true: nothing rotates those two credentials afterwards, and the operator key is a
file a person must carry off the machine — the installer says so and cannot check it.
**Nothing asserts a mesh was installed this way.** A claim that a machine was brought up by this
procedure cannot be contradicted by anything afterwards.
+62 -22
View File
@@ -1,7 +1,7 @@
---
layer: to-be
status: designed
code: []
status: in-progress
code: [mesh-catalog, mesh-controller]
updated: 2026-09-20
decisions:
- 02-DECISIONS/0085-a-secret-is-a-provision.md
@@ -57,21 +57,64 @@ This is what replaces the "generated value that nothing owns". A module's own pa
a special kind of thing injected by the synchroniser and becomes a provision with a provider, a
holder, and a lifecycle — the same shape as everything else the mesh grants.
## The vault is a module, and node-scoped
## The vault is a foundation module, one per mesh
The vault is an ordinary module. A mesh that wants one runs it; a mesh whose modules require no
secret of their own runs none — the same test identity meets
([ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md)). It is provisioned
the ordinary way, and it does **not** mint the mesh's delivery credentials — the controller does
that, because the controller must mint in order to deliver any provision, the vault's own included.
The vault mints secrets *for modules*, downstream of its own existence, never the credential that
delivers it.
The vault is an ordinary module — built, assigned and upgraded like any other — and it is part of
the foundation: installed at genesis, the way the store and broker are raised first and then
adopted as modules ([ADR 0078](../../02-DECISIONS/0078-the-store-and-broker-are-modules.md)). A
mesh has root secrets, so a mesh has a vault; it is not something a mesh opts into
([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), amended). It is provisioned the
ordinary way, and it does **not** mint the mesh's delivery credentials — the controller does that,
because the controller must mint in order to deliver any provision, the vault's own included.
Like every provider it is node-scoped
([ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), [23](23-choosing-a-provider.md)):
each node may run its own vault, and a module's own secret is held by the vault on the module's
node, selected the same way any provider is. There is no single mesh vault holding everything, for
the same reason there is no single mesh store.
There is **one vault per mesh**, on the control-node, reached from any node the way identity is.
Node scoping ([ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md),
[23](23-choosing-a-provider.md)) exists because a store holds data a consumer is coupled to; a
vault holds nothing a consumer is coupled to, and a second one would be a second place to lose.
## The root secrets, and the operator key
The mesh has secrets of its own that no module requires from anybody: the store's superuser, the
broker's administrator, the controller's credentials to its contexts, the sealing key of every
node. Until now each was sealed to the node that uses it and to nothing else, so a node whose key
was gone took them with it — and at genesis they were not even secret, the foundation being raised
with fixed credentials that were then carried in.
The vault's second job is to hold these, and it does so without holding a value:
- **An operator sealing key.** A keypair made once, by the operator, whose private half is written
to a file the operator keeps off the mesh and whose public half the mesh records. It is made
before anything else is minted, so that everything is sealed to it.
- **A second seal.** Every secret a module holds for itself — minted or accepted — is sealed to its
node as before and, in addition, to the operator key. What the mesh stores is one more blob it
cannot open. A secret made before the key existed has no such copy and cannot get one, the
plaintext being gone; the mesh says which those are rather than letting the export pass for
complete.
- **The export.** All copies sealed to the mesh's current operator key, the key's public half and
its fingerprint, and two honest lists beside them: what is sealed to an earlier key the mesh has
since replaced, openable with that key alone, and what has no operator copy at all. As one
document. The vault declares that it *keeps* this, and
the mesh writes it onto the vault's own disk as an ordinary declared file — ciphertext to the
machine that holds it and to the bus it crossed. The same document can be written out by the
operator to keep beside the key.
- **Recovery.** The operator, holding the private key, opens one secret from the export or from the
store and gets it as a file, never on a terminal unless asked for. It needs the key and a blob;
the mesh has the blobs and no key, the operator the key and no blob until given one. Nothing in
this reintroduces a place that can open everything, which is the property
[ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md) was
built to keep — so the break-glass question the first version of this design left open is
answered by it.
**Genesis** makes the operator key first and mints real root secrets in place of the fixed ones the
foundation is raised with, so the mesh is handed over with nothing well-known in it. That step is
the installer's and is not yet built; until it is, the fixed credentials are the as-is and are
said so in [21](21-the-installation-in-full.md).
A module's vault-provided secret — the pair credential of
[13](13-credentials-and-their-rotation.md) — is sealed to the operator the same way, as is every
credential a provider grants; the export names each entry by the node and module that hold it and
the name they know it by, and says whether it is a module's own secret or a pair credential, so
recovery addresses both alike.
## Beyond generate and hold
@@ -80,13 +123,10 @@ secrets subsystem whose surface names the operations a vault is responsible for
backing it up, verifying it is what it should be, and a break-glass recovery for when the normal
path is unavailable. These become the vault module's, specified against it rather than scattered.
One of them is left open on purpose. **Break-glass must not reintroduce a key that one place
holds.** The whole point of sealing a secret to the machine that needs it, asymmetrically, is that
no single place can open everything
([ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md)); a
recovery path that keeps a master key would undo exactly that. How the vault lets an operator
recover a secret without becoming the thing the sealing was designed to prevent is a question this
design opens and does not yet answer.
Break-glass is answered above, and by the property it had to keep: **it does not reintroduce a
key that one place holds.** The vault keeps blobs sealed to the operator; the operator keeps a key
with nothing to open until handed a blob. Locating and verifying are the vault's tools, by
fingerprint; backing up is the export.
## What this changes for a module
@@ -4,6 +4,7 @@ opened: 2026-08-23
located-in: [hal]
fixed-by:
- "the instance only: PR #962 — incus hook. Merged, ran on one node in 6s of a 60s budget; pipeline #6832 green in 48s. The class remains open."
- "partly, on the mesh: mesh-host 73c010e, 9d8239a — preflight and the profile detectors ask the daemon, not the package (installed-but-broken reports absent). The class question the report asks of hal is not answered"
amended-design:
---
@@ -1,8 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-01
located-in: [mesh-control, mesh-host]
fixed-by: partly — mesh-control ee3cc1b
fixed-by: mesh-controller ee3cc1b (pinned() refuses a placeholder at compose), 43c9c97 (the builder publishes digests); proven by one-node-mesh.test.ts (the catalogue holds no placeholder digest). Six placeholders remain in three manifests deferred under issue 064
amended-design:
---
@@ -2,7 +2,7 @@
status: located
opened: 2026-09-01
located-in: [mesh-control]
fixed-by: partly — mesh-control 53eb000, withdrawn in 83c6a2f
fixed-by: partly — mesh-controller f5b03e1 declares the data directories; the gate refusing a container mount the module never declared (53eb000) was withdrawn in 83c6a2f and nothing replaces it
amended-design:
---
@@ -1,8 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-01
located-in: [mesh-host, mesh-control]
fixed-by:
fixed-by: mesh-host aa441ba, 652984b (container restart-on, PR #2, under issue 009); proven by mesh-lab runtime-restart-on-config.test.ts
amended-design:
---
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-04
located-in: []
fixed-by:
fixed-by: mesh-host aa441ba (restart-on); mesh-catalog 550393b (runtime config as a mergeable file + restart-on); proven by mesh-lab runtime-restart-on-config.test.ts
amended-design:
---
@@ -2,7 +2,7 @@
status: open
opened: 2026-09-02
located-in: []
fixed-by:
fixed-by: partly — mesh-host 24e9ae4 (a run-once step writes a seed only if absent, ADR 0052); mesh-catalog ec1e718 (mosquitto). A file resource itself has no create-once semantic, and no bed asserts a seed's grown content survives a re-apply
amended-design:
---
@@ -1,8 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-02
located-in: [mesh-control, mesh-catalog, mesh-host]
fixed-by: 02-DECISIONS/0051-shared-data-is-the-operators.md
fixed-by: ADR 0051 access: mesh-controller aeb65a3, mesh-host f06eea5, mesh-catalog 69c8a5e; proven by mesh-lab assigned-catalogue-media.test.ts (sonarr and radarr share one operator-owned directory)
amended-design: 02-DECISIONS/0051-shared-data-is-the-operators.md
---
@@ -1,8 +1,8 @@
---
status: located
status: resolved
opened: 2026-09-05
located-in: [mesh-control, mesh-host, mesh-catalog]
fixed-by: 02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md
fixed-by: ADR 0052 run-once container: mesh-host 19e5dd8, PR #5 24e9ae4; ADR 0053 scheduled twin 9d1f001; proven by mesh-lab assigned-catalogue-mqtt.test.ts
amended-design: 02-DECISIONS/0052-a-step-that-runs-once-before-a-container.md
---
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-10
located-in: []
fixed-by:
fixed-by: mesh-host cmd/mesh-bootstrap (ADR 0067); mesh-lab genesis.ts is the one description of genesis both beds call. Still open: a running mesh cannot say it was raised by the installer
amended-design:
---
@@ -56,3 +56,12 @@ either way.
- What is the actual reach of the exposure on a node — which identities can talk to the container
runtime, and is that set smaller than "anything running as the operator"? The answer decides
whether this is a hardening item or something sharper.
## Reconciled 2026-09-21
Still open, and worse than reported: the controller's own manifest now delivers its store
connections **and** its broker credentials through an env-file, so both halves reach the process
environment; 28 catalogue manifests use env-file for a secret, and nothing in the catalogue
engine refuses a `${secret:…}` placeholder in one. The vault work of
[ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md) rests on the seal this weakens.
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-13
located-in: []
fixed-by:
fixed-by: mesh-host e7f94e0 (restart-on digests folded into the container spec, a standing comparison); unit-tested in apply_test.go, no lab assertion yet
amended-design:
---
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-14
located-in: []
fixed-by:
fixed-by: mesh-controller c8d8211 (forward chain drops by default, never closes ssh), 25e42b3; proven by one-node-mesh.test.ts (a machine filters exactly what its modules declared)
amended-design:
---
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-14
located-in: []
fixed-by:
fixed-by: mesh-controller 3ae7b88, 2b82872; mesh-catalog d2dce34; proven by one-node-mesh.test.ts (the catalogue holds every module the control plane built)
amended-design:
---
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-14
located-in: []
fixed-by:
fixed-by: mesh-controller dda001d (the firewall opens the port the mesh itself runs on), 25e42b3; TestTheBrokersPortIsOpenedThoughNoModuleDeclaresIt
amended-design:
---
@@ -1,8 +1,8 @@
---
status: open
status: resolved
opened: 2026-09-15
located-in: []
fixed-by:
fixed-by: mesh-tools b618057 (the SDK resolved by version from the registry, one pin); mesh-host 7986306; mesh-controller 4b9bc50. Caveat: the base image still builds with npm install and no lock, so the build is not reproducible
amended-design:
---
@@ -2,7 +2,7 @@
status: resolved
opened: 2026-09-20
located-in: []
fixed-by:
fixed-by: graduated — hq fc4ab37 (ADR 0084, to-be design 23)
amended-design: 03-DESIGN/01-to-be/23-choosing-a-provider.md
---
@@ -2,7 +2,7 @@
status: resolved
opened: 2026-09-20
located-in: []
fixed-by:
fixed-by: graduated — hq fc4ab37 (ADR 0085, to-be design 24)
amended-design: 03-DESIGN/01-to-be/24-the-secrets-vault.md
---
@@ -0,0 +1,57 @@
---
status: open
opened: 2026-09-20
located-in: []
fixed-by:
amended-design:
---
# One `secret` provision yields one value, and a module may need several
## Symptom, as observed
[ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md) makes a module's own secret a
provision: the module requires `secret` from a vault and reads the pair credential the mesh
minted for that consumer↔vault pair. A pair has **exactly one** credential — that is the property
[13](../../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md) is built on, and the reason
rotation touches one holder and nothing else.
A module may only require a provision name once, so a module that requires `secret` receives
**one value**. Counted across the catalogue at the time of writing, a module's own secrets other
than its broker account number as follows:
| own secrets | modules |
|---|---|
| one | 22 |
| two | 6 (an internal token and an admin password, a user and a password for an outbound mail relay, …) |
| three or more | 4 (a source, an admin and a relay password; a root certificate, its key and that key's password; …) |
Ten modules cannot be migrated onto the vault as designed, because the design gives them one
value where they hold two or more, and nothing in the record says what they should do instead.
## Why it matters beyond this instance
- **The migration in ADR 0085 is "module by module, not a flag day"** — but for a third of the
modules with own secrets there is no target to migrate to, and the gap is silent: such a
module keeps `own-secrets` for the rest and looks migrated.
- **Two candidate answers pull in different directions and neither is recorded.** A module could
derive several keys from its one value (a key-derivation step inside the module, which puts
cryptography into every module that needs two secrets), or the mesh could let a consumer
require several *named* secrets from one vault (which is a pair with several credentials, the
thing 13 deliberately does not have, or several pairs between the same two modules, which the
identity derivation in [ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)
cannot express).
- **A secret with structure is not one value either.** A certificate authority's root
certificate, key and key password are three things with one lifecycle; a value the controller
mints at random is none of them. The vault's third species — an operator-delivered secret — is
the nearer fit, and it has its own gap ([070](../070-an-operator-cannot-deliver-a-pair-credential/00-report.md)).
## Open questions
- Is "one value per module" a rule to keep, with derivation the module's business, or does a
consumer name several secrets and the vault serve each as its own pair?
- If the latter: what is the login of the second pair between the same consumer and the same
vault, given that a login is derived from the (node, module) pair and is what the secret is
keyed on?
- Which modules genuinely need several independent secrets, and which hold two names for one
thing (an admin password and a token that is only ever set from it)?
@@ -0,0 +1,54 @@
---
status: open
opened: 2026-09-20
located-in: [mesh-controller]
fixed-by:
amended-design:
---
# An operator cannot deliver a pair credential, so the vault's third species has no entry
## Symptom, as observed
[ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md) names three species of secret and
gives the vault two: a module's own secret, which the mesh mints, and an **operator-delivered**
secret — a credential for something outside the mesh, which only a person can supply. The design
([24](../../03-DESIGN/01-to-be/24-the-secrets-vault.md)) says the vault *"takes custody of one an
operator delivered"*.
Under 0085 a secret from the vault is a pair credential between the consumer and the vault. The
controller has one command that takes a value from a person — `secret accept` — and it writes
**only a module's own secret**: one node, one module, one name, sealed to that node's key alone.
There is no command that accepts a value *into a pair*: sealed to the consumer's node **and** to
the vault's node, recorded as `accepted` so a later push does not replace it with a minted one.
The primitive exists. The sealing package's `Accept` takes a value and two keys, and the licence
adapters already use it to carry an operator's token to both ends of a licence. Nothing exposes it
for an ordinary provision.
So today an operator-delivered secret can be held in only one of two wrong ways: as an own secret
(un-audited, un-rotatable — the gap 0085 opened to close), or not at all.
## Why it matters beyond this instance
- **The vault is one third short of its decision** until this exists, and the third that is
missing is the one with a real leak history — external keys are the secrets that end up in
transcripts and env files.
- **The `made`/`accepted` distinction is only recorded for own secrets.** A pair credential has no
origin column, so even once a value can be accepted into a pair, `rotate` would mint over it —
generating "32 random bytes where a working credential was", the exact fault `secret accept`
was written to prevent for own secrets. Rotating an accepted pair credential must refuse, or
must ask for the replacement value, and neither is designed.
- **The consumer's login is derived, and an external service did not derive it.** An accepted
external key has no `as` the far side knows; a pair credential assumes both ends agree on one.
For the vault this is harmless (the vault creates nothing under that login), but the model
leaks through in what the consumer is told.
## Open questions
- Is this `secret accept <consumer-node> <module> secret --from …` growing a provider end, or a
new verb on the pair?
- Does an accepted pair credential refuse `rotate`, or does `rotate` become "accept a new value
and deliver it" for that pair?
- Should the origin (`made` / `accepted`) become a fact of every pair credential, so the vault's
ledger can show which of its secrets a person supplied?
@@ -0,0 +1,41 @@
---
status: resolved
opened: 2026-09-20
located-in: [mesh-host internal/bootstrap, mesh-host examples/foundation-first-node.lock]
fixed-by: mesh-host feat/secrets-vault (ee0c8b8, genesis root credentials + operator key + vault); mesh-controller feat/secrets-vault (e140ed5, 565f144); mesh-catalog feat/secrets-vault; proven by the one-node genesis bed step V5
amended-design: 03-DESIGN/01-to-be/24-the-secrets-vault.md
---
# The foundation is raised with fixed credentials, and they stay
## Symptom, as observed
The foundation bundle raises the store with a superuser password that is the literal word
`bootstrap`, and the broker with its image's default administrator, `guest` / `guest`. The
installer then carries both into the mesh through `secret accept`, sealed to the control-node's
key, marked `accepted` so the mesh will never replace them — which is correct for a credential
that already created the databases, and means the well-known value is now permanent.
Every module's own secret minted afterwards is random and sealed. The two that everything else
rests on are not random, and there is no operator key at genesis for anything to be sealed to.
## Why it matters beyond this instance
- **These are the root secrets.** A mesh whose store superuser is a published constant is a mesh
whose every provisioned credential is one connection away, from any node that can reach 5432.
- **It is invisible.** `secret accept` reports the value as sealed to the machine and unreadable by
the mesh, which is true, and says nothing about where it came from.
- **Rotation cannot fix it later.** An accepted own secret is never remade by the mesh, and there is
no `rotate` for own secrets; the only path is to change it on the server by hand and accept it
again, which is the manual rotation the as-is design records as having taken services down.
## What closes it
[ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), amended, and design
[24](../../03-DESIGN/01-to-be/24-the-secrets-vault.md): genesis makes the operator key first,
mints real credentials for the store and broker before the bundle raises them (or changes them
on the running servers before handing over), accepts those, and installs `mesh-vault` so the
operator-sealed export exists from the first push. Built on `feat/secrets-vault` across mesh-host, mesh-controller and mesh-catalog, and proven by
the one-node genesis bed: the template's password is refused by the store, the export and the
vault's copy hold no plaintext, and the superuser recovered off the mesh with the operator key
opens the store.