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:
Binary file not shown.
@@ -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
|
git worktree add .work/<slug>/<repo> -b feat/<slug> origin/main
|
||||||
```
|
```
|
||||||
Parallel features never collide, and no shared checkout is edited.
|
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
|
3. **Commit as you go — locally.** Increments land on the one branch. Nothing is pushed and no
|
||||||
MR is opened mid-feature.
|
MR is opened mid-feature.
|
||||||
4. **Finish, then publish.** When the whole feature is done — every repo, tests green — push
|
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
|
- 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.
|
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
|
- 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
|
merged, is the failure this playbook exists to prevent — stop and consolidate before opening
|
||||||
more.
|
more.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
topic: what runs on it
|
topic: what runs on it
|
||||||
status: accepted
|
status: accepted
|
||||||
date: 2026-09-20
|
date: 2026-09-20
|
||||||
|
amended: 2026-09-20
|
||||||
deciders: jochen
|
deciders: jochen
|
||||||
reconstructed: false
|
reconstructed: false
|
||||||
extends: 02-DECISIONS/0031-the-control-plane-authenticates-nobody.md
|
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
|
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.
|
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
|
## References
|
||||||
|
|
||||||
- [ADR 0031](0031-the-control-plane-authenticates-nobody.md) — identity is a module; this is the
|
- [ADR 0031](0031-the-control-plane-authenticates-nobody.md) — identity is a module; this is the
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ code:
|
|||||||
- mesh-catalog modules/postgres
|
- mesh-catalog modules/postgres
|
||||||
- mesh-catalog modules/lavinmq
|
- mesh-catalog modules/lavinmq
|
||||||
- mesh-lab test/integration/mesh.test.ts (a bare machine becomes a mesh)
|
- mesh-lab test/integration/mesh.test.ts (a bare machine becomes a mesh)
|
||||||
updated: 2026-09-17
|
updated: 2026-09-21
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
|
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
|
||||||
- 02-DECISIONS/0078-the-store-and-broker-are-modules.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.
|
the module that provides one.
|
||||||
- **Whether one host can raise all three.** The claim under stage 2 of
|
- **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 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
|
- **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.
|
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 internal/bootstrap
|
||||||
- mesh-host cmd/mesh-bootstrap
|
- mesh-host cmd/mesh-bootstrap
|
||||||
- mesh-lab test/integration/one-node-mesh.test.ts
|
- mesh-lab test/integration/one-node-mesh.test.ts
|
||||||
updated: 2026-09-15
|
updated: 2026-09-21
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0067-genesis-is-a-pivot.md
|
- 02-DECISIONS/0067-genesis-is-a-pivot.md
|
||||||
- 02-DECISIONS/0073-the-installer-carries-a-builder.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
|
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.
|
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
|
**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.
|
procedure cannot be contradicted by anything afterwards.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: designed
|
status: in-progress
|
||||||
code: []
|
code: [mesh-catalog, mesh-controller]
|
||||||
updated: 2026-09-20
|
updated: 2026-09-20
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0085-a-secret-is-a-provision.md
|
- 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
|
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.
|
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
|
The vault is an ordinary module — built, assigned and upgraded like any other — and it is part of
|
||||||
secret of their own runs none — the same test identity meets
|
the foundation: installed at genesis, the way the store and broker are raised first and then
|
||||||
([ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md)). It is provisioned
|
adopted as modules ([ADR 0078](../../02-DECISIONS/0078-the-store-and-broker-are-modules.md)). A
|
||||||
the ordinary way, and it does **not** mint the mesh's delivery credentials — the controller does
|
mesh has root secrets, so a mesh has a vault; it is not something a mesh opts into
|
||||||
that, because the controller must mint in order to deliver any provision, the vault's own included.
|
([ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), amended). It is provisioned the
|
||||||
The vault mints secrets *for modules*, downstream of its own existence, never the credential that
|
ordinary way, and it does **not** mint the mesh's delivery credentials — the controller does that,
|
||||||
delivers it.
|
because the controller must mint in order to deliver any provision, the vault's own included.
|
||||||
|
|
||||||
Like every provider it is node-scoped
|
There is **one vault per mesh**, on the control-node, reached from any node the way identity is.
|
||||||
([ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), [23](23-choosing-a-provider.md)):
|
Node scoping ([ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md),
|
||||||
each node may run its own vault, and a module's own secret is held by the vault on the module's
|
[23](23-choosing-a-provider.md)) exists because a store holds data a consumer is coupled to; a
|
||||||
node, selected the same way any provider is. There is no single mesh vault holding everything, for
|
vault holds nothing a consumer is coupled to, and a second one would be a second place to lose.
|
||||||
the same reason there is no single mesh store.
|
|
||||||
|
## 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
|
## 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
|
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.
|
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
|
Break-glass is answered above, and by the property it had to keep: **it does not reintroduce a
|
||||||
holds.** The whole point of sealing a secret to the machine that needs it, asymmetrically, is that
|
key that one place holds.** The vault keeps blobs sealed to the operator; the operator keeps a key
|
||||||
no single place can open everything
|
with nothing to open until handed a blob. Locating and verifying are the vault's tools, by
|
||||||
([ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md)); a
|
fingerprint; backing up is the export.
|
||||||
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.
|
|
||||||
|
|
||||||
## What this changes for a module
|
## What this changes for a module
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ opened: 2026-08-23
|
|||||||
located-in: [hal]
|
located-in: [hal]
|
||||||
fixed-by:
|
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."
|
- "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:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-01
|
opened: 2026-09-01
|
||||||
located-in: [mesh-control, mesh-host]
|
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:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
status: located
|
status: located
|
||||||
opened: 2026-09-01
|
opened: 2026-09-01
|
||||||
located-in: [mesh-control]
|
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:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-01
|
opened: 2026-09-01
|
||||||
located-in: [mesh-host, mesh-control]
|
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:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-04
|
opened: 2026-09-04
|
||||||
located-in: []
|
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:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
status: open
|
status: open
|
||||||
opened: 2026-09-02
|
opened: 2026-09-02
|
||||||
located-in: []
|
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:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-02
|
opened: 2026-09-02
|
||||||
located-in: [mesh-control, mesh-catalog, mesh-host]
|
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
|
amended-design: 02-DECISIONS/0051-shared-data-is-the-operators.md
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-05
|
opened: 2026-09-05
|
||||||
located-in: [mesh-control, mesh-host, mesh-catalog]
|
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
|
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
|
opened: 2026-09-10
|
||||||
located-in: []
|
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:
|
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
|
- 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
|
runtime, and is that set smaller than "anything running as the operator"? The answer decides
|
||||||
whether this is a hardening item or something sharper.
|
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
|
opened: 2026-09-13
|
||||||
located-in: []
|
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:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-14
|
opened: 2026-09-14
|
||||||
located-in: []
|
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:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-14
|
opened: 2026-09-14
|
||||||
located-in: []
|
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:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-14
|
opened: 2026-09-14
|
||||||
located-in: []
|
located-in: []
|
||||||
fixed-by:
|
fixed-by: mesh-controller dda001d (the firewall opens the port the mesh itself runs on), 25e42b3; TestTheBrokersPortIsOpenedThoughNoModuleDeclaresIt
|
||||||
amended-design:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-15
|
opened: 2026-09-15
|
||||||
located-in: []
|
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:
|
amended-design:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
status: resolved
|
status: resolved
|
||||||
opened: 2026-09-20
|
opened: 2026-09-20
|
||||||
located-in: []
|
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
|
amended-design: 03-DESIGN/01-to-be/23-choosing-a-provider.md
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
status: resolved
|
status: resolved
|
||||||
opened: 2026-09-20
|
opened: 2026-09-20
|
||||||
located-in: []
|
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
|
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.
|
||||||
Reference in New Issue
Block a user