diff --git a/docs/swarm/credentials.md b/docs/swarm/credentials.md index e1ecfbcf..75a092ca 100644 --- a/docs/swarm/credentials.md +++ b/docs/swarm/credentials.md @@ -11,12 +11,13 @@ gets deleted and this file moves into its place. Until then the two coexist and neither is wrong: that one says where a file is, this one says whether it should be a file at all. -Two conventions carry over unchanged, because the target doesn't relax them. +Two conventions frame everything below. -**A secret is a path, never a value.** Every option carrying key material or a -token takes a file path, because a literal in a nix expression lands in the nix -store — world-readable and permanent. That rule survives the migration; what -changes is how many paths there are. +**Where a nix option does carry a secret, it takes a path, never a value.** A +literal in a nix expression lands in the nix store — world-readable and +permanent. The target doesn't relax that rule; it removes almost every option +the rule applies to, because in the target shape there is almost nothing on +disk for an option to point at. **Public material is a value.** A certificate or a public nkey is handed to every client that connects, so the store is a fine place for it. Nothing below @@ -73,34 +74,56 @@ not credentials this page governs. Their absence isn't an oversight. ## Target state -⚠️ **This section describes what the swarm is moving toward — not what exists -today.** Read the current-state table above for that. In particular, the first -line below states intent, not current state, so it's stated as a target twice. +⚠️ **This section describes the shape the swarm is meant to have — not what +exists today, and not a claim of movement toward it.** Read the current-state +table above for what exists. Nothing here is partly done. -**Every host needs a store mTLS certificate. Everything else is done through -the store.** That's the target. Today only the host running the store itself -mints its own leaves automatically — for itself, and pre-emptively for a controller and a -publisher that might be co-located. For every other host, `swarm-bao.nix:529-531` -says it outright: this is _"the credential an operator places by hand."_ No -automated issuance path exists for an ordinary hive. The target is one -out-of-band credential per host and zero out-of-band credentials anywhere else; -the current state is one out-of-band credential per host **plus** every file in -the last column of the table above. +**No secret the store holds is ever written to disk.** That's the invariant, +and everything else in this section follows from it. A value pulled from the +store — bao — lives in the memory of the process that asked for it and nowhere +else: not in a state directory, not in a bind-mounted file, not in a systemd +credential, not in a rendered config, not for a moment before a unit deletes +it. The target isn't a shorter list of secret files. It's the store, plus +exactly one file. -**Agent containers get an mTLS certificate passed in from the hive, and pull -their secrets themselves.** The hive's job becomes handing the agent an -identity, not handing it secrets. Today no agent has an identity of any kind -and every agent secret arrives as a file the host wrote. +**That one file is the mTLS client certificate, and it's the only credential on +disk.** It has to be a file, and the reason is the whole asymmetry: the +certificate is what authenticates a principal to the store, so it's the one +credential that can't be fetched from the store. Something has to exist on disk +before the first request, or there's nothing to make the request with. Every +host has one, and its only power is to ask the store for the rest. Today only +the host running the store mints its own leaves automatically — for itself, and +pre-emptively for a controller and a publisher that might be co-located. For +every other host, `swarm-bao.nix:529-531` says it outright: this is _"the +credential an operator places by hand."_ -**No store-provided secret is persisted.** A value read from the store lives in -the memory of the process that needs it and nowhere else — not in a state -directory, not in a bind-mounted file, not in a systemd credential. Today every -one of the four is persisted, and one of them twice. +**The hive hands an agent an identity, never a secret.** An agent container is +passed an mTLS certificate from its hive and from then on authenticates to the +store under its own name, pulling what it needs when it needs it. No process +reads a secret on another principal's behalf: the principal that needs a value +is the principal that authenticates for it. -**Per secret, the docs specify minter, reader, and renewal strategy.** Those -three columns are the contract. A secret whose renewal strategy is `NONE` is -allowed to exist only as an honest record of where the swarm is — it's not a -strategy, and a new secret doesn't get to write it. +**Per secret, the target specifies minter, reader, and renewal strategy.** +Those three are the contract, and in this shape the **reader is a process +pulling a store path at runtime** — not a path on disk, and not a unit whose +job is to turn a store value into a file. + +| store path | minter | reader — pulls at runtime, holds in memory | renewal | +| -------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| `swarm/agents//matrix/` | `swarm-controller` | the agent container itself, under the certificate its hive passed in | **must be stated** — no strategy is chosen for this credential, and `NONE` isn't one | +| `swarm/hives//matrix/appservice-token` | one minter, on the authelia host | the hive process that presents the token to its homeserver, under the hive's own certificate | **must be stated** — as above | +| `swarm/hives//queue/agent` | authelia | the agent container presenting the OIDC client to the swarm queue, under its own certificate | **must be stated** — as above | +| `swarm/services//oidc/client` | authelia | the service process that presents the client secret, under the certificate of the host it runs on | **must be stated** — as above | +| _(not in the store)_ the mTLS client leaf | the store's own PKI, or an operator placing it by hand | its own client, off disk — **the sole exception above**, because it's what makes every other row's pull possible | **must be stated** — as above | + +A renewal cell reading `NONE` is allowed only in the current-state table above, +as an honest record of where the swarm is. It isn't a strategy, and a new +secret doesn't get to write it. + +The appservice token's second, uncoordinated minter isn't a row here for a +reason: in the target each credential has exactly one minter, so a fallback +mint on the reading host is a thing the target deletes, not a thing it +describes. ## Progressive enhancement