The secrets vault: handoff, the amended decision, issues 069–071, and the reconciliation of open issues #58
@@ -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
|
||||
|
||||
@@ -246,6 +246,12 @@ 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.** [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md),
|
||||
amended, makes the vault a foundation module: genesis makes the operator key before anything
|
||||
is minted, replaces the fixed credentials the store and broker are raised with, and installs
|
||||
`mesh-vault` beside the adopted store and broker ([24](24-the-secrets-vault.md)). The controller's
|
||||
half exists; the installer's does not yet, and the bundle still raises the foundation with
|
||||
well-known credentials ([issue 071](../../04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md)).
|
||||
- **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-20
|
||||
decisions:
|
||||
- 02-DECISIONS/0067-genesis-is-a-pivot.md
|
||||
- 02-DECISIONS/0073-the-installer-carries-a-builder.md
|
||||
@@ -118,6 +118,11 @@ 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 is raised with fixed credentials, and they stay.** The store's superuser and the
|
||||
broker's administrator are constants in the bundle, carried into the mesh by `secret accept`. No
|
||||
operator key is made at genesis, so nothing minted during installation is sealed to one
|
||||
([24](24-the-secrets-vault.md), [issue 071](../../04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md)).
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-catalog]
|
||||
code: [mesh-catalog, mesh-controller]
|
||||
updated: 2026-09-20
|
||||
decisions:
|
||||
- 02-DECISIONS/0085-a-secret-is-a-provision.md
|
||||
@@ -57,21 +57,60 @@ 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 operator-sealed copies, the key's public half and its fingerprint, and the
|
||||
list of what is not recoverable, 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).
|
||||
|
||||
What is not yet sealed to the operator: a module's vault-provided secret — the pair credential of
|
||||
[13](13-credentials-and-their-rotation.md). That is the next increment, the same column and the
|
||||
same call on the pair table.
|
||||
|
||||
## Beyond generate and hold
|
||||
|
||||
@@ -80,13 +119,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
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
status: located
|
||||
opened: 2026-09-20
|
||||
located-in: [mesh-host internal/bootstrap, mesh-host examples/foundation-first-node.lock]
|
||||
fixed-by:
|
||||
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. The controller's half — the key, the second
|
||||
seal, export and recovery — is built; the installer's half is not.
|
||||
Reference in New Issue
Block a user