Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)

This commit is contained in:
2026-10-16 07:37:43 +00:00
parent 1a2c917abf
commit 9652b2d5ce
771 changed files with 52063 additions and 1774 deletions
+132 -17
View File
@@ -76,17 +76,19 @@ Two boxes on the mesh today, both over X25519:
What the vault uses:
- **Its answers to `get` are the ADR 0206 box, for every asker, a node included.** The asker sends its
public key as SPKI PEM. A node's provisioner client (V4) needs no second key: its node's sealing key is
X25519 already, so it sends that key's public half wrapped as SPKI and opens the answer with the private
half wrapped as PKCS #8 (`TestANodesSealingKeyOpensTheAnswer` does exactly this). A raw 32-byte key is
refused: a raw private key looks the same, and the vault takes no private key.
public key as SPKI PEM. A node's provisioner client (V4) asks with a key of its own, made when it starts
and kept in its memory alone: it runs under an account of its own and cannot read the node-engine's sealing
key, which is root's. A node's sealing key would open an answer too, being X25519 (wrapped as SPKI and PKCS #8,
`TestANodesSealingKeyOpensTheAnswer`), for the node-engine's own asks. A raw 32-byte key is refused: a raw
private key looks the same, and the vault takes no private key.
- **Its operator copies are the NaCl anonymous box**, to the operator key as it is made today, so the
operator's recovery opens them with the key and the command it already has.
- **The at-rest copy is the ADR 0206 box**, to the vault's own key.
The NaCl box stays on the node-engine only for what the controller seals into declarations, until the last
module has moved (ADR 0349 §17). For the core's own credentials that travel in a declaration sealed by the
vault (ADR 0349 §16), V4 adds opening the ADR 0206 box with the same sealing key to the node-engine.
vault (ADR 0349 §16), the node-engine is to open the ADR 0206 box with the same sealing key; that is not built
yet (issue 647 leaves it to its own issue).
## The seat's verb
@@ -102,15 +104,107 @@ bounded: rolled at 1 MiB into `handed.jsonl.1` … `.3`, the oldest dropped, eac
journal and counted in `vault_status`. `secret_handed` reads it.
`vault_status` says what needs a person: operator copies still sealed to an earlier key, an operator key in
the export the vault refused, and how often the record was rolled. **It is not yet a condition**: a health
check is declared on a long-running resource (ADR 0240), and the vault is a bundle the tool runner loads, so
nothing asks it on its own; until it runs as a resource of its own (V3's review) these reach a person
through the journal and this tool only.
the export the vault refused, and how often the record was rolled.
**Who may ask, today: nobody.** The rule (ADR 0349 §11) is a node's provisioner client under a bus account
of its own, for what its node's declaration names, and a module under its own account (issue 569). Neither
exists yet, so the running vault refuses every asker; the tests set a stand-in list. The real rule goes
behind the `Askers` interface in `askers.go` (design 57's V4 and P3).
**A failing re-seal is a condition** (ADR 0366, issue 738). The vault re-seals its operator copies to the
operator key once a minute. `vault_health` answers unhealthy once that has failed on three passes in a row,
saying what failed and why, and healthy again after one pass succeeds. The manifest declares it as the
health of the vault's own runtime (`runtime-health`), which the controller puts on `mesh-vault.own-runtime`;
the node-engine asks it every minute and the controller raises `module.mesh-vault.<machine>.unhealthy` from
it, cleared on the first healthy answer. An operator key in the export the vault refuses fails the pass too.
How often the record was rolled still reaches a person through the journal and `vault_status` only.
**Who may ask: a node's provisioner client, for what its plan names** (ADR 0349 §11, design 57 V4, issue 647;
`askers.go`). The caller is `provisioner.<node>`, the client's own account (never the tool runner's, which an agent
on that machine shares), and the secret is `own/<node>/<module>/<name>` for that same machine, where the
controller's assignment `<node>.<module>` (the assignments mirror, which the manifest `reads`) carries the
provisioner's plan and the plan's `secrets` name it; or a value a provider's software issued for one holder,
`provision/<provider>/<provision>/<node>/<module>`, where that plan's `provisions` name the provision from that
provider with a credential's `name` (issue 757; an entry naming only the provider serving a provision, issue 760,
hands nothing). The vault cannot read a node's declaration, so the plan the
controller carries beside it is what the declaration names, as the vault reads it. A provision's one credential,
`provision/<provider>/<provision>`, is handed to the machines the vault made it for (below) while the holder there
is handed to the provisioner: the provider's module, or a consumer whose plan still names it from that provider. A
module not handed to the provisioner, another machine's secret or holder value, and every other caller are refused,
and each refusal is recorded with why. A module's own account (issue 569) is not answered yet.
**The controller, for the core's own credentials, sealed only to their machine** (ADR 0349 §16, issue 763;
`core.go`). The bus and mesh-provisioner need their files before any client runs, so their credentials travel in
the declaration sealed by the vault to their machine, which the controller carries and cannot open. The caller
`controller` is answered `own/<node>/<module>/<name>` only for a module on the core's fixed list (`nats`,
`mesh-provisioner`, `mesh-provisioner-client`, the controller's list too), and only when the key it names is that
machine's sealing key as its own node-engine stated it in its own name, in `move-in`'s first step (`sealing_key`).
A machine whose engine stated none, any other key, any other module and any other name are refused and recorded.
## Taking values in: `accept`, `secret-ask`, `issued`, `rotate`
Design 57's V2 (issue 675; ADR 0349 clauses 8 to 10, ADR 0277 §22–§27 and ADR 0305 with the vault in the
controller's place). `intake.go`.
**From the controller alone; the vault asks the desk and the provider itself.** The controller alone asks these
verbs, as the runtime proves its caller. Each value comes in sealed to an X25519 key the vault made for that one
call (raw, base64 — the NaCl anonymous box the desk's prompt and a provider's tool already seal to), and opens with
the private half, which lives in this process only and is forgotten once used or after two minutes. A value sealed
to any other key is refused, and a call is answered once.
**The vault's own account** (issue 723). The vault runs as `mesh-vault` in a runtime of its own (`runs-as`, ADR
0259 §8), on a bus account of its own (`<node>.mesh-vault`, from its own secret `broker`). The bus lets that
account call the desks' prompt (`node-launcher.secret` on any machine) and each issuing provider's tool in its own
name, and nothing else outside its seat (mesh-controller's `internal/broker` grants, proven on a real server). So
the prompt and the tool know the vault asks, and the controller carries neither the key nor the sealed value. The
controller still asks them itself for a module filled the old way, until R1 (issue 682) removes that path; they
answer both until then. Everything the vault keeps is its account's: its directories, the export it keeps, the
grant files the controller still writes for it.
- `accept` — two steps: node, module, name (a module's own secret) or provider, provision (a provision's one
credential), and `by`; then `call` and `sealed`. The controller's terminal seals the value it read to the call's
key before it leaves the terminal. Kept with origin `accepted`; an accepted provision credential is never remade
(`rotate` refuses it); a value a provider's software issued is not taken by `accept`.
- `secret-ask` — one step: node, module, name, desk and `asked_by` (who asked the controller, as the bus named its
caller). The ask is recorded in `asks.jsonl` before anything else, and refused while another ask for the same
secret is open (two minutes at most) or when three were made in the last hour. The vault then asks the desk's
prompt itself, naming who asked as the record has it, with `timeout_seconds` 20. The answer is how it ended
(`given`, dismissed, timed-out, empty, refused, failed) and who asked; how each ask ended is recorded.
`secret_asks` reads the record. The hand-act log stays the controller's: it records the act once the vault has
taken the value.
- `issued` — one step: provision, provider (machine), module and tool. The vault asks that tool on the provider's
machine itself; the value is kept as the provision's one credential with origin `issued`. The same value again
writes nothing, says nothing and keeps the generation (ADR 0305 §5).
- `issued` for each holder apart (issue 749; ADR 0349 clause 11, ADR 0365 clause 4: the forge's read token per agent
account). The controller names the provision's holders from that provider (`holders`, each a node and a consuming
module), and the vault asks the tool with them. The tool answers either one value (`sealed`, kept as above) or one
per holder (`holders`: node, module and `sealed` each, all sealed to the call's key). Each is kept as
`provision/<provider node>/<provision>/<holder node>/<holder module>`, origin `issued`, and the same value again for
a holder writes nothing for it. The answer is taken whole or not at all: a holder not asked about, one named twice, a
value not sealed to the call or an empty one keeps nothing. A holder asked about and not answered is said
(`not_answered`) and keeps nothing. **`get` hands a holder's value to that holder alone**, its machine's provisioner
client (`provisioner.<node>`) or the module under an account of its own (`<node>.<module>`), whatever the rule for
who may ask allows; every other caller is refused and the refusal recorded.
`TestOnARealBusTwoHoldersOnTwoMachinesEachGetOnlyTheirOwnIssuedValueAndNoneCrossesInTheClear` proves it on a NATS
server with a stand-in forge.
- `rotate` — a module's own secret or a provision's credential, made anew. Refused for an `issued` value (the
software rotates it, ADR 0305 §9) and an accepted provision credential (clause 8). From the controller alone,
which knows whether the module's secrets are filled from the vault yet, and with a `why`, said in the vault's
log. Who asked is the `by` the controller names, beside the controller; both are kept (`changed-by`,
`changed-via`). The controller's `rotate <provision>` forwards here for a provider filled the new way (issue
767): a shared value is made anew for every holder, never for one consumer alone; for a provider whose consumers
keep a login each, `consumer` and `consumer_module` (`--consumer`, `--module`) name the one consumer whose own
login is remade, and no other is touched.
Names in the store: `own/<node>/<module>/<name>`, `provision/<provider node>/<provision>` and, for a value issued for
one holder, `provision/<provider node>/<provision>/<holder node>/<holder module>`. V3 copies the
controller's rows in under these names.
**Which modules reach the vault this way.** The controller sends a module's verbs here only when its secrets are
filled the new way (its machine's provisioner client asks the vault for them, V4); every other module keeps the
controller's path, so no value lands where its machine never looks (ADR 0349 clause 17; the release lead's decision
on issue 675).
**What the issuing provider's test is here.** `TestAnIssuingProviderAnswersOnlyTheVaultsCallSealedToTheVault` and
`TestTheVaultAsksTheIssuingToolItselfAndOnlyASealedValueCrosses` use a stand-in provider that answers the vault's
account (and, until R1, the controller) and no other caller, and only sealed to the call's key. plex's own tool
answers the controller alone today; it must also answer `<node>.mesh-vault` when it moves with the media catalogue,
after release 57.
## Events
@@ -149,11 +243,28 @@ and a holder's ledger file removed only through a person's delete, as here. The
lives: the harness kept it in its process, so a restart forgot it (ADR 0230 §13); here it is in the ledger
file.
**The credential a provider shares with its consumers** (ADR 0349 clause 8, ADR 0158 §4–§6, design 57 V5, issue
677; `pair.go`). One value per provider assignment and provision, `provision/<provider>/<provision>`. Once a minute
the vault reads every plan in the assignments mirror; each plan's `provisions` names the provider serving each
provision its module requires (issue 760). A consumer binds when an entry appears and unbinds when it goes or its
assignment goes; on either, the vault makes one new value for every holder together and says `secret-changed`, and
each holder's client asks again. What the move brought in (V3) is adopted when every pair row of a provider and
provision shares one generation stamp and one value (ADR 0158's shared form), with its origin, and nothing is
remade. Rows without a stamp (ADR 0048, a login per consumer) are never made one value (ADR 0158 §11): `rotate` of
the provision remakes each consumer's own apart, and each consumer's machine is handed only its own row,
`pair/<provider>/<provision>/<consumer>/<module>[/<local>]` (the provider's machine all of them). A provider the
vault holds no row of gets its first value in the form each plan entry names (`credential-form`, from the provider's
offer, issue 768): `shared`, one value for every holder; `pair`, a login of its own for each consumer bound; `issued`
or `given` are never made. No form named, plans that disagree, or a form it does not know: nothing is made. An accepted or issued value is never remade, only
its holders recorded. A provision whose plans name a credential per holder (issue 757) has no shared value. **No
value is made that a holder would never get:** while the provider's assignment, or a consumer the move brought in,
is still filled the old way (no plan), every make, remake and rotation is held back and the reason said in the
log. Who holds each value is kept in the store's `shared/` directory, never a value.
## What comes later
Taking values in (`secret accept`, `secret-ask`, `issued`, `rotate`) is V2; the controller's secrets move
in with V3; nodes ask with V4; pair credentials are minted here with V5. Until V3 the vault's store is
empty and the controller still serves every secret.
Taking values in is built (above); the controller's secrets move in with V3; nodes' provisioner clients ask with V4,
for each module handed to the provisioner; providers answer their consumers with B1–B3.
## Tests
@@ -161,4 +272,8 @@ empty and the controller still serves every secret.
`vault_test.go` is ADR 0349's vault test list as far as this package goes: no bucket holds a secret; no
private key is accepted; every secret has an operator-sealed copy; `get` answers only an allowed asker,
sealed to its key; `secret-changed` carries no value.
sealed to its key; `secret-changed` carries no value. `intake_test.go` is ADR 0349's `secret-ask` test as far as
the vault answers it (recorded before the prompt, one open ask per secret and three an hour, the asker named as
the bus named it, the typed value opened only here), the issuing provider's test with a stand-in provider (ADR 0305
§13's checks that belong to the vault: nothing rewritten for a value held, `rotate` refused, no other way in), and
accept and rotate. `followups_test.go` holds V1's review follow-ups.