diff --git a/docs/README.md b/docs/README.md index 298a78c3..1ae0fda8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -77,7 +77,7 @@ declarations. 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 is that shape headed?** → [`swarm/credentials.md`](swarm/credentials.md) + 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. diff --git a/docs/swarm/credentials.md b/docs/swarm/credentials.md index 692a7bba..7e14abea 100644 --- a/docs/swarm/credentials.md +++ b/docs/swarm/credentials.md @@ -1,17 +1,17 @@ -# Credentials: current state, target state, and the rule in between +# Credentials: current state, target state, and the rule between the two Where [`secrets.md`](secrets.md) is a map of every file a swarm holds, this page is the **direction of travel**: what the credential shape is today, what -it is meant to become, and the test a change has to pass to count as movement +it's on track to become, and the test a change has to pass to count as movement toward it rather than away. -It is written to be the thing a discussion points at. **When the migration +It's the thing a discussion should point at. **When the migration below is complete, this page supersedes the old credential docs** — `secrets.md` 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 does not relax them. +Two conventions carry over unchanged, because the target doesn't relax them. **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 @@ -25,10 +25,10 @@ is about those. ## Current state Four credential families live in the swarm's secret store, plus the mTLS -identity that opens it. That fifth row is not a stored secret — it is the -credential that cannot be, being what authenticates the reader to the store. +identity that opens it. That fifth row isn't a stored secret — it's the +credential that can't be, being what authenticates the reader to the store. -| store path | what it is | minter | writer | reader | renewal | persisted outside the store | +| store path | kind | minter | writer | reader | renewal | persisted outside the store | | -------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `swarm/agents//matrix/` | one agent's matrix account credential | `swarm-controller`, via a caller-supplied bearer token or its own `m.login.password` (`swarm-controller/src/matrix_account.rs:14-21`) | same unit, `put_matrix_account`'s `store.write` call (`swarm-controller/src/matrix_account.rs:189-197`) | `hive-c0re`'s credential worker, under the **hive's** identity (`hive-c0re/src/workers/credential.rs:33-64`) | `NONE` | **yes** — `hive-priv` writes `/matrix-token`, `0600`, chowned to the agent (`hive-priv/src/main.rs:535-556`) | | `swarm/hives//matrix/appservice-token` | the appservice token authenticating one hive's homeserver | `swarm-secret-publisher.nix` itself, `head -c 32 /dev/urandom` on the authelia host (`nix/host-modules/swarm-secret-publisher.nix:271-278`) | same unit (`nix/host-modules/swarm-secret-publisher.nix:288-289`) | `swarm-bao-matrix-token.service` on the hive (`nix/host-modules/glue-matrix-bao-token.nix:159-167`) | `NONE` | **yes, twice** — the publisher's own state copy (`nix/host-modules/swarm-secret-publisher.nix:185`) and the hive's token file, `0600` (`nix/host-modules/glue-matrix-bao-token.nix:180-181`) | @@ -36,7 +36,7 @@ credential that cannot be, being what authenticates the reader to the store. | `swarm/services//oidc/client` | a swarm service's own OIDC client secret, one per service | authelia, on first boot | `swarm-secret-publish.service` (`nix/host-modules/swarm-secret-publisher.nix:245-248`) | whichever host runs that service — `swarm-bao-grafana-oidc.service` (`nix/host-modules/swarm-grafana.nix:565-684`), the collector's twin (`nix/host-modules/swarm-otel.nix:685-767`) | `NONE` | **yes** — Grafana's `0400` host file (`nix/host-modules/swarm-grafana.nix:678-682`), the collector's `0400` file handed in by `LoadCredential` (`nix/host-modules/swarm-otel.nix:764-768`) | | _(not stored)_ the store's own mTLS leaves | the certificate a host presents to reach the store at all | `swarm-bao-pki.service`, self-signed, 10-year (`nix/host-modules/glue-bao-tls.nix:114-149`) | n/a | every client reads its own leaf off disk via `BAO_CLIENT_CERT` (`swarm-secret-client/src/client.rs:12-14`) | `NONE` — signed only when absent (`nix/host-modules/glue-bao-tls.nix:128-149`) | **always, by construction** — it has no other form | -Four things in that table are uncomfortable, and they are the reason this page +Four things in that table are uncomfortable, and they're the reason this page exists rather than a note in the margin of the old one. **Every stored secret is persisted as a plaintext file outside the store.** All @@ -44,15 +44,15 @@ four, without exception. The appservice token is persisted twice: once in the publisher's own state directory on the authelia host, and again in the token file on the hive that reads it. -**Nothing rotates.** Every renewal cell reads `NONE`. There is no rotation path +**Nothing rotates.** Every renewal cell reads `NONE`. No rotation path exists for any of the four, and none for the mTLS leaves either — those are signed once if absent and carry ten years of validity, and deleting the CA to reissue invalidates every client certificate already trusting it. -**No agent container has a store identity at all.** There is not one -`BAO_ADDR` or `BAO_CLIENT_CERT` anywhere under `nix/agent-modules/`. An agent +**No agent container has a store identity at all.** Not one +`BAO_ADDR` or `BAO_CLIENT_CERT` exists anywhere under `nix/agent-modules/`. An agent never talks to the store; the hive host reads on the agent's behalf and writes -a file into the container. That is the exact inverse of the target below. +a file into the container. That's the exact inverse of the target below. **The appservice token has a second, uncoordinated minter.** `hive-matrix.nix` keeps a local first-boot mint as a fallback for an unreachable store. Two @@ -68,22 +68,22 @@ fetched bytes to disk. Two per-agent credential files — the forge token and the github token (`hive-priv/src/main.rs`'s `write_forge_token`/`write_github_token`) — are deliberately absent from the table. -They are operator-supplied and never pass through the store at all, so they are -not credentials this page governs. Their absence is not an oversight. +They're operator-supplied and never pass through the store at all, so they're +not credentials this page governs. Their absence isn't an oversight. ## Target state -⚠️ **This section describes what the swarm is moving toward. None of it is a -description of what exists today.** Read the current-state table above for -that. In particular, the first line below is the one most easily misread as a -statement of fact, so it is stated as a target twice. +⚠️ **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 is the one most often misread as a statement of fact, so it's +stated as a target twice. **Every host needs a store mTLS certificate. Everything else is done through -the store.** That is the target. Today only the host running the store itself -auto-mints leaves — for itself, and pre-emptively for a controller and a +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."_ There -is no automated issuance path for an ordinary hive. The target is one +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. @@ -100,38 +100,38 @@ one of the four is persisted, and one of them twice. **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 is not a -strategy, and a new secret does not get to write it. +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. ## Progressive enhancement The rule has two halves, and the second is the one that makes the first survivable. -**New functionality matches the target shape immediately.** There is no -grandfather clause for code that does not exist yet. +**New functionality matches the target shape immediately.** No legacy clause +covers code that doesn't exist yet. **Existing functionality moves step by step** — each individual step moving -toward the target shape, never sideways and never away. A step does not have to +toward the target shape, never sideways and never away. A step doesn't have to finish the migration. It has to not undo it. -That is checkable, so check it. Apply these to a pull request that touches a +That's checkable, so check it. Apply these to a pull request that touches a credential: | question | a `yes` means | | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | Does it add a minter outside the swarm's existing mint path? | **block** for new functionality. For existing functionality, only if it removes one too. | | Does it persist a store-provided secret to disk — a state directory, a bind mount, a rendered config? | **block** for new functionality. For existing functionality, only if the change removes a persisted copy elsewhere and nets out fewer. | -| Does it add a credential whose renewal strategy is `NONE`? | **block** for new functionality. State the strategy, even if it is "operator reissues and restarts the reader." | +| Does it add a credential whose renewal strategy is `NONE`? | **block** for new functionality. State the strategy, even if it's "operator reissues and restarts the reader." | | Does it read a secret on another principal's behalf and hand it over? | **block** for new functionality. The principal that needs the value authenticates for it. | -| Does it give a host or container an out-of-band credential that is not the store mTLS leaf? | **block**, both. One out-of-band credential per principal is the whole point of the store. | +| Does it give a host or container an out-of-band credential that's not the store mTLS leaf? | **block**, both. One out-of-band credential per principal is the whole point of the store. | | Does it change the minter, reader, or renewal of an existing credential? | The current-state table above is **stale until updated in the same change.** | -For existing functionality the questions are not pass/fail — they are a +For existing functionality the questions aren't pass/fail — they're a direction check. A change that persists one fewer copy, or moves one reader from a host-written file to its own store read, is a step and lands. A change that -leaves the count the same while moving the file somewhere tidier is not a step; -it is churn with a migration story attached. +leaves the count the same while moving the file somewhere tidier isn't a step; +it's churn with a migration story attached. When the last `yes` in the current-state table's final column becomes a `no`, this page replaces `secrets.md` and the current-state section goes with it.