From baa33515521b033dd84b1a8fff458c637ffcfca9 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 20 Sep 2026 23:55:01 +0200 Subject: [PATCH] Amend ADR 0085: the vault is a foundation module and holds the root secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recorded on the record, dated, before anything shipped against the sentences that change. The vault is installed at genesis like the store and broker, one per mesh, and holds every secret a module has for itself sealed a second time to an operator key whose private half never enters the mesh — the break-glass path the first version left open, without a key one place holds. Design 24 says how; 07 and 21 say what genesis does not yet do; issue 071 names the fixed credentials the foundation is raised with today. --- 02-DECISIONS/0085-a-secret-is-a-provision.md | 46 +++++++++++ 03-DESIGN/01-to-be/07-the-foundation.md | 6 ++ .../01-to-be/21-the-installation-in-full.md | 7 +- 03-DESIGN/01-to-be/24-the-secrets-vault.md | 78 ++++++++++++++----- .../00-report.md | 39 ++++++++++ 5 files changed, 154 insertions(+), 22 deletions(-) create mode 100644 04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md diff --git a/02-DECISIONS/0085-a-secret-is-a-provision.md b/02-DECISIONS/0085-a-secret-is-a-provision.md index ca0c54e..f322231 100644 --- a/02-DECISIONS/0085-a-secret-is-a-provision.md +++ b/02-DECISIONS/0085-a-secret-is-a-provision.md @@ -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 diff --git a/03-DESIGN/01-to-be/07-the-foundation.md b/03-DESIGN/01-to-be/07-the-foundation.md index 87d7751..7b6587c 100644 --- a/03-DESIGN/01-to-be/07-the-foundation.md +++ b/03-DESIGN/01-to-be/07-the-foundation.md @@ -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. diff --git a/03-DESIGN/01-to-be/21-the-installation-in-full.md b/03-DESIGN/01-to-be/21-the-installation-in-full.md index 91c5dae..80a6406 100644 --- a/03-DESIGN/01-to-be/21-the-installation-in-full.md +++ b/03-DESIGN/01-to-be/21-the-installation-in-full.md @@ -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. diff --git a/03-DESIGN/01-to-be/24-the-secrets-vault.md b/03-DESIGN/01-to-be/24-the-secrets-vault.md index 082fed3..9b1070e 100644 --- a/03-DESIGN/01-to-be/24-the-secrets-vault.md +++ b/03-DESIGN/01-to-be/24-the-secrets-vault.md @@ -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 diff --git a/04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md b/04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md new file mode 100644 index 0000000..433826e --- /dev/null +++ b/04-ISSUES/071-the-foundation-is-raised-with-fixed-credentials/00-report.md @@ -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.