95 lines
5.4 KiB
Markdown
95 lines
5.4 KiB
Markdown
# mesh-vault
|
|
|
|
Holds the `mesh-vault` seat: the mesh's shared secrets, each kept sealed, handed out only on request and
|
|
reply, sealed to whoever asked (novox/hq ADR 0085, ADR 0113, ADR 0349 §7 and §11). Go, one binary
|
|
(`cmd/mesh-vault`), launched by its node's runtime and speaking MCP to it through the Go SDK.
|
|
|
|
## Where the secrets are kept, and why there
|
|
|
|
In the vault's own store: one file per secret in `${dir:store}` on the vault's own disk, mode 0600 in a
|
|
0700 directory. Not a database, as most Go modules keep their data, because:
|
|
|
|
- the vault is installed before any other module built on the runtime base (ADR 0113 §14), and a database
|
|
login is itself a shared secret the vault makes — the vault cannot need one to start;
|
|
- every value in the store is ciphertext anyway, so a database would add nothing a file does not;
|
|
- no secret is in a bucket, sealed or not (ADR 0201 §13), so the module declares no `state`.
|
|
|
|
Each record holds the value twice, never in the clear:
|
|
|
|
- **at rest**, sealed to the vault's own key (the sealed box of ADR 0206, `mesh-sdk/go/seal`). The key is
|
|
an X25519 pair the vault makes once in `${dir:state}/vault-key.json`; its private half never leaves it.
|
|
- **for the operator**, sealed to the operator key (ADR 0113 §22), so nothing exists only inside the
|
|
vault. A secret is refused, loudly, while the vault has no operator key.
|
|
|
|
## Where the operator key comes from
|
|
|
|
From the export the mesh writes to the vault's root directory (`${dir:root}/export.json`, the manifest's
|
|
`keeps`): its `operator-key` is the public half `operator key set` recorded in the controller. The vault
|
|
checks it every minute; a new key makes the vault seal every operator copy again to it — it can, because
|
|
it opens its own copy. The operator key's private half never enters the mesh, and the vault refuses one
|
|
(ADR 0113 §2). When the secrets move to the vault (design 57's V3), the operator key moves with them.
|
|
|
|
## The boxes and the key formats — settled
|
|
|
|
Two boxes on the mesh today, both over X25519:
|
|
|
|
| | format | keys | who uses it |
|
|
|---|---|---|---|
|
|
| NaCl anonymous box | XSalsa20-Poly1305, `box.SealAnonymous` | raw 32 bytes, base64 | the controller sealing into declarations; the node-engine's `sealing.key` (mesh-host `internal/identity/sealing.go`); the operator key and `secret recover --from-export` |
|
|
| the sealed box (ADR 0206) | X25519, HKDF-SHA256, AES-256-GCM, `{v, eph, iv, tag, ct}` | PEM: SPKI public, PKCS #8 private | the licence manager and `claude-code`; `mesh-sdk/go/seal` |
|
|
|
|
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.
|
|
- **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.
|
|
|
|
## The seat's verb
|
|
|
|
`mesh-vault.get` — `name`, `public_key` (SPKI PEM). It answers `{name, generation, changed-at, sealed}`,
|
|
`sealed` being the secret sealed to that key, and only to an asker allowed for that secret, the asker being
|
|
the bus user the runtime proved — never an argument.
|
|
|
|
**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).
|
|
|
|
## Events
|
|
|
|
- `secret-changed` — `name`, `by`, `at`, `generation`: which secret, who changed it, when. Never a value
|
|
or a sealed copy.
|
|
- `provisioned`, `rotated`, `deprovisioned` — the custody ledger's, by fingerprint, as before.
|
|
|
|
## The tools
|
|
|
|
`secret_export`, `secret_holders`, `secret_verify` answer as the TypeScript vault's did, and its custody
|
|
ledger (`${dir:ledger}`, the same JSON files) is kept by a pass over the contributions file every minute.
|
|
None returns a value.
|
|
|
|
What the TypeScript vault did and this one does not: it consumed its own three events only to print an
|
|
audit line; the pass now prints that line itself.
|
|
|
|
## 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.
|
|
|
|
## Tests
|
|
|
|
go test ./...
|
|
|
|
`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.
|