Audited all 22 file:line citations against source on this head, per argus's PR review. Fixed: - matrix_account.rs writer cell pointed at path-computation line 163; the actual store.write() call is at 189-197 (now anchored to the put_matrix_account function name too). - swarm-secret-publisher.nix appservice-token minter cited the "put on every run" comment (280-286) instead of the mint itself (271-278). - glue-matrix-bao-token.nix persisted-token cell cited the registration-restamp comment (186-188) instead of the printf+chmod (180-181). - glue-queue-agent-credential.nix persisted-files cell missed the secret's chmod 0600 at 219, citing 224-231; corrected to 218-224. - swarm-otel.nix persisted-file cell cited 763-767, one line short of the chmod 0400 at 768; corrected to 764-768. - hive-priv/src/main.rs:368,413 (forge/github token cell) landed in unrelated DestroyContainer/ListContainers/WriteAgentGithubToken match arms; replaced with the write_forge_token/write_github_token function names as a greppable, edit-resistant anchor. - swarm-bao.nix:527-528 (the "operator places by hand" quote) missed the quoted text, which starts at 529; corrected to 529-531. 15 of 22 already resolved exactly; no claim lacked supporting code, so nothing was removed. 22 of 22 citations checked.
137 lines
14 KiB
Markdown
137 lines
14 KiB
Markdown
# Credentials: current state, target state, and the rule in between
|
|
|
|
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
|
|
toward it rather than away.
|
|
|
|
It is written to be the thing a discussion points 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.
|
|
|
|
**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.
|
|
|
|
**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.
|
|
|
|
## 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.
|
|
|
|
| store path | what it is | minter | writer | reader | renewal | persisted outside the store |
|
|
| -------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `swarm/agents/<agent>/matrix/<account>` | 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 `<agent-state>/matrix-token`, `0600`, chowned to the agent (`hive-priv/src/main.rs:535-556`) |
|
|
| `swarm/hives/<hive>/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`) |
|
|
| `swarm/hives/<hive>/queue/agent` | the OIDC client id + secret agent containers present to the swarm queue | authelia, on first boot, into its own client-secret dir | `swarm-secret-publish.service` (`nix/host-modules/swarm-secret-publisher.nix:222-225`) | `swarm-bao-queue-agent.service` on the hive (`nix/host-modules/glue-queue-agent-credential.nix:187-197`) | `NONE` | **yes** — `secret` `0600` and `client_id` `0644` under the hive's agent-credential dir (`nix/host-modules/glue-queue-agent-credential.nix:218-224`) |
|
|
| `swarm/services/<clientId>/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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
|
|
**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
|
|
minters exist for one logical credential, on different hosts, with no agreement
|
|
between them — they can diverge, and the only thing that reconciles them is the
|
|
reader overwriting the local value on its next successful start.
|
|
|
|
One half of the target is already real: all four families are minted once and
|
|
read by an identity that logs in with a certificate, with no ambient trust
|
|
anywhere in the path. The cleanest examples are `swarm-bao-queue-agent.service`
|
|
and `swarm-bao-grafana-oidc.service`. What each of them then does is write the
|
|
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.
|
|
|
|
## 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.
|
|
|
|
**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
|
|
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
|
|
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.
|
|
|
|
**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.
|
|
|
|
**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.
|
|
|
|
**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.
|
|
|
|
## 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.
|
|
|
|
**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
|
|
finish the migration. It has to not undo it.
|
|
|
|
That is 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 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 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
|
|
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.
|
|
|
|
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.
|