diff --git a/docs/README.md b/docs/README.md index 3483f42a..1ae0fda8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -76,6 +76,10 @@ declarations. - **Where do agent snapshots go? How does the swarm's `btrfs receive` endpoint authenticate a pushing hive?** → [`networking/snapshot-store.md`](networking/snapshot-store.md). +- **Who mints each credential, who reads it, and how does it rotate — and + where's that shape headed?** → [`swarm/credentials.md`](swarm/credentials.md) + (current state, target state, and the progressive-enhancement rule); + [`swarm/secrets.md`](swarm/secrets.md) for where each file lives today. ## Scheduler, CI, observability diff --git a/docs/swarm/README.md b/docs/swarm/README.md index 3d085e0f..feea18bd 100644 --- a/docs/swarm/README.md +++ b/docs/swarm/README.md @@ -104,6 +104,12 @@ Every credential the swarm holds, who mints it, where it must live, and which of the three topologies makes it the operator's job to place: [`secrets.md`](secrets.md). +Where that shape is **going** — the per-secret minter/reader/renewal +contract, the target of one mTLS identity per host and everything else +through the store, and the test a change has to pass to count as movement +toward it: [`credentials.md`](credentials.md). It supersedes `secrets.md` +when the migration completes. + ## Swarm UI The operator-only web surface on the swarm apex, why reaching it needs diff --git a/docs/swarm/credentials.md b/docs/swarm/credentials.md new file mode 100644 index 00000000..680848b6 --- /dev/null +++ b/docs/swarm/credentials.md @@ -0,0 +1,77 @@ +# Credentials: the target shape + +The swarm's credential store is bao. This page describes the shape every +credential is meant to have — who mints it, who reads it, and how it renews +— not what's on disk today. [`secrets.md`](secrets.md) remains the map of +the files that exist right now; this page replaces it, and `secrets.md` gets +deleted, once the swarm's credential path matches what's described below. + +**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 is about those. + +**No secret the store holds is ever written to disk.** That's the invariant, +and everything else in this page 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 one file per identity. + +**Those files are mTLS client certificates, one per identity, and they're +the only credential on disk.** Each 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 identity — an agent, a swarm-level service — +needs one; a host running several holds several, and its only power is to +ask the store for the rest. +`swarm-bao.nix:529-533` states the rule for the nix option that carries it: +this is _"the credential an operator places by hand"_, and _"a path, never a +value."_ A literal in a nix expression lands in the nix store — +world-readable and permanent — so that option takes a path to the +certificate on disk, never the certificate's bytes. + +**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. + +Two per-agent credential files — the forge token and the github token — sit +outside this page: they're operator-supplied and never pass through the +store, so the table below doesn't govern them. + +**Per secret, the target specifies minter, reader, and renewal strategy.** +Those three are the contract, and 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. A renewal cell may never read `NONE`: state the +strategy for every credential, including the mTLS leaf. + +| 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 | +| `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 | +| `swarm/hives//queue/agent` | authelia | the agent container presenting the OIDC client to the swarm queue, under its own certificate | must be stated | +| `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 | +| _(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 | + +## Touching a credential + +A pull request that touches a credential can't: + +- add a minter outside the swarm's existing mint path +- persist a store-provided secret to disk — a state directory, a bind mount, + a rendered config +- add a credential whose renewal strategy is `NONE` — state the strategy, + even if it's "operator reissues and restarts the reader" +- read a secret on another principal's behalf and hand it over — the + principal that needs the value authenticates for it +- give a host or container an out-of-band credential that isn't the store + mTLS leaf — one out-of-band credential per principal is the whole point of + the store + +While the swarm's credential path is still moving to this shape, a PR that +moves a secret into bao is allowed to leave its renewal strategy unresolved, +provided it opens a follow-up issue to settle renewal. That's a migration-era +allowance, not a standing exception to the renewal-strategy rule above.