120 KiB
| this hive's cache pull facts | swarm-controller, into the secret store at swarm/hives/<hive>/attic/{pull-token,url,cache,public-key}, copied every five minutes from swarm/services/attic/ | hive-c0re reads them under the hive's own store identity and writes /run/hyperhive/attic/netrc (the pull token), substituter and public-key (tmpfs, each 0600 hive-core). Only the probe reads them; hive-priv doesn't. |
Credentials: the per-secret reference
Every credential in the swarm's secret store, in one table: who mints it, who
reads it, and whether it renews on its own. The overview and the rules a
credential follows are in credentials.md; this page is
the detail, so it changes whenever someone adds a credential or moves its minting.
The second half, where each secret lives on disk, maps the
files that exist today.
In the table, automatic re-mint is something replacing the stored value without an operator. Automatic re-pull is the reader picking up a replaced value without a restart. ✅ means it happens today, ❌ means it doesn't, and the rest of the cell says how.
| store path | minter | reader | automatic re-mint | automatic re-pull |
|---|---|---|---|---|
swarm/agents/<agent>/matrix/main |
swarm-controller, at agent creation and every 5 minutes |
the agent | ✅ when missing or rejected by the homeserver | ✅ the daemon restarts within 5 minutes of a rejection |
swarm/agents/<agent>/matrix/<account> |
swarm-controller, from a credential you hand it |
the agent | must be stated | ✅ retried within 2 to 16 minutes after a rejection |
swarm/agents/<agent>/matrix-recovery/<account> |
swarm-controller, a random passphrase per account |
the agent | ❌ never replaced: the account's key backup opens only with this passphrase | ❌ read when the account comes up |
swarm/controller/swarm-controller/matrix/appservice-token |
swarm-matrix-ctl appservice sync in hive-matrix, from the registration's as_token |
swarm-controller |
❌ follows the registration below | ✅ read every 5 minutes |
swarm/controller/swarm-controller/matrix/appservice-registration |
swarm-matrix-ctl appservice sync in hive-matrix: seeded from the container's own tokens when the store holds none, a new pair only when the container has none either |
swarm-matrix-ctl in hive-matrix; the controller has no grant to it |
❌ minted once; an operator who replaces it makes sync write the new pair into the container and restart the homeserver |
✅ sync re-reads it every 10 minutes |
swarm/controller/swarm-controller/oidc/client |
authelia, first boot | swarm-controller |
❌ minted once; swarm-secret-publish republishes a re-mint |
✅ re-read when authelia refuses it |
swarm/controller/swarm-controller/forge/token |
the forge, once | swarm-controller |
❌ minted once; delete /var/lib/forgejo/swarm-controller-token-name in the forge container to mint a new one |
✅ re-read on a 401 |
swarm/controller/swarm-controller/grafana/token |
swarm-bao-grafana-controller-token on Grafana's host |
swarm-controller |
✅ every 10 minutes, when Grafana refuses it or none is stored | ✅ re-read on a 401 |
swarm/controller/swarm-controller/forge/webhook-secret |
swarm-controller, at start, from its old webhook-secret file; a new one only when there is no file |
swarm-controller |
❌ minted once; a rotated secret fails every delivery until the forge hooks are re-registered | ❌ read at start |
swarm/agents/<agent>/bao-mtls |
the store's agent PKI mount (deploy.bao.agentPkiMountPath), at agent creation |
hive-c0re, then the agent |
✅ re-issued every 5 minutes once past half its 7-day validity | ✅ the agent re-reads every 10 minutes; it renews only while its current leaf is valid |
swarm/agents/<agent>/queue |
swarm-controller, at agent creation |
the agent; the queue, log and metrics auth services | ✅ re-minted at 45 days; skipped for Destroyed agents, whose path is deleted |
✅ read on every reconnect; an open connection keeps the old secret for up to an hour; the log and metrics stores honour a replaced secret for up to deploy.victorialogs.authVerdictTtl and deploy.victoriametrics.agentAuthVerdictTtl (60 s by default) |
swarm/agents/<agent>/forge-token |
swarm-controller, at agent creation and every 5 minutes |
the agent | ✅ when missing or no longer matching the forge | ✅ every 10 minutes |
swarm/agents/<agent>/forge/<label> |
swarm-controller, when you link an external forge account in the swarm UI |
the agent | ❌ your token; delete it and link the label again | ✅ every 2 minutes |
swarm/agents/<agent>/github-token |
swarm-controller, when you link a GitHub account in the swarm UI |
the agent | ❌ your token; delete it and link again | ✅ every 2 minutes |
swarm/agents/<agent>/backend-env |
hive-agent-backend-env, in the agent's container, from the agent's backendEnvironmentFile when the leaf holds nothing (field env, the file's exact text); or swarm-controller, when you set the API-key environment in the swarm UI |
the agent | ❌ nothing replaces it without you; a save in the swarm UI overwrites it, including text an agent wrote into an empty leaf; the agent's file stays as it was | ❌ the file in /run refreshes every 2 minutes, and the harness and the subagent daemon load it at their next start |
swarm/agents/<agent>/claude-credentials |
swarm-claude-login, when you log the agent in from its card; one version per login |
the agent, while it waits for a login | ❌ log in again | ✅ every 30 seconds while waiting |
swarm/hives/<hive>/matrix/sender-token |
swarm-controller, every 5 minutes |
swarm-controller and the hive |
✅ when missing or rejected by the homeserver | ✅ the hive re-reads on every sweep |
swarm/hives/<hive>/forge/token |
swarm-controller, every 5 minutes |
swarm-controller and the hive |
✅ when missing or no longer matching the forge | ✅ read on every forge call |
swarm/agents/<agent>/proxy-credential |
swarm-controller: 32 random bytes, at start and every 5 minutes for every agent that holds a store identity |
the hive (hive-c0re); the agent can read it, nothing sends it yet |
❌ never replaced: written only while the path holds nothing | ✅ hive-c0re re-reads it every 30 seconds |
swarm/hives/<hive>/squid-selfcheck |
swarm-controller, in the same pass as the agents' credentials |
the hive: hive-c0re and squid-selfcheck-credential (root) |
❌ never replaced: written only while the path holds nothing | ✅ hive-c0re every 30 seconds, the root unit every 10 minutes |
swarm/hives/<hive>/oidc/client |
authelia, first boot; swarm-secret-publish copies it in |
the hive | ❌ minted once; swarm-secret-publish republishes a re-mint |
✅ re-read when authelia refuses it |
swarm/services/<clientId>/oidc/client |
authelia | that service | must be stated | must be stated |
swarm/services/ci-runner/registration-token |
swarm-controller, from the forge, at start and every 5 minutes |
swarm-bao-ci-runner on the runner's host |
❌ the forge keeps one token until an admin resets it; the controller then writes the new one within 5 minutes | ✅ every 10 minutes; restarts the runner on a change |
swarm/services/<grafana client id>/metrics/bearer |
swarm-bao-grafana-metrics-bearer on Grafana's host, when none is stored |
swarm-metrics-auth and swarm-logs-grafana-auth |
❌ delete it and restart swarm-bao-grafana-metrics-bearer |
✅ the auth services re-read within a minute; ❌ Grafana restarts when its copy changes |
swarm/services/<grafana client id>/secret-key |
swarm-bao-grafana-secret-key on Grafana's host, from the key an earlier release generated; a new one only when there is none |
Grafana | ❌ minted once; a different key leaves Grafana unable to read its stored datasource secrets | ❌ Grafana reads it at start; the unit restarts Grafana when the file changes |
swarm/services/swarm-authelia/keys/<name>, for jwt, session, storage-encryption, oidc-hmac and oidc-issuer |
authelia's authelia-swarm-secrets unit in its container, under the secret publisher's identity, from the key file an earlier release generated; a new one only when there is none |
authelia | ❌ minted once; a different storage-encryption key leaves authelia's database unreadable and a different issuer key invalidates every issued token | ❌ read when the unit runs; the unit restarts authelia when the file changes |
swarm/services/review-classifier/anthropic-api-key |
you, in the store's UI, field api_key |
swarm-review-classifier |
❌ write a new key | ✅ read on every run |
swarm/services/review-classifier/acp-provider |
you, in the store's UI, field api_key |
swarm-review-classifier |
❌ write a new key | ✅ read on every run |
swarm/services/review-classifier/forge/token |
swarm-controller, at start and every 5 minutes |
swarm-controller |
✅ when missing or no longer matching the forge | ✅ read on every review |
swarm/services/builder/forge/token |
swarm-controller, at start and every 5 minutes |
swarm-builder, swarm-controller |
✅ when missing or no longer matching the forge | ✅ read on every build |
swarm/services/attic/push-token |
swarm-bao-attic-tokens on the cache's host, when none is stored; with an external cache, you, in the store's UI (field value), or bao kv put with value=- and the token on stdin |
swarm-builder |
❌ write a new token; a token the cache refuses fails the push, and the build stays green with cached false |
✅ read on every push; with both swarm.attic options null, nothing reads it |
swarm/services/attic/pull-token |
swarm-bao-attic-tokens on the cache's host, when none is stored; read-only, ten years; with an external cache, you, in the store's UI (field value), or bao kv put with value=- and the value on stdin |
swarm-controller, to copy it and to check narinfos |
❌ write a new token | ✅ copied to the hives every 5 minutes |
swarm/services/attic/url |
swarm-bao-attic-tokens on the cache's host, when none is stored (https://<swarm.attic.domain>); with an external cache, you, in the store's UI (field value), or bao kv put with value=- and the value on stdin (scheme included, no trailing slash) |
swarm-controller, which copies it to each hive |
❌ delete it and restart swarm-bao-attic-tokens, or write the new value |
✅ copied to the hives every 5 minutes |
swarm/services/attic/cache |
swarm-bao-attic-tokens on the cache's host, when none is stored (swarm.attic.cache); with an external cache, you, in the store's UI (field value), or bao kv put with value=- and the value on stdin |
swarm-controller, which copies it to each hive |
❌ as the URL | ✅ copied to the hives every 5 minutes |
swarm/services/attic/public-key |
swarm-bao-attic-tokens on the cache's host, when none is stored (the key attic cache info prints); with an external cache, you, in the store's UI (field value), or bao kv put with value=- and the value on stdin (<name>:<base64>) |
swarm-controller, which copies it to each hive |
❌ as the URL. A cache recreated with a new key leaves the stored one stale: every hive then reports bad-signature |
✅ copied to the hives every 5 minutes |
swarm/hives/<hive>/attic/{pull-token,url,cache,public-key} |
swarm-controller, from the four swarm/services/attic/ leaves, every 5 minutes; it skips a leaf whose source holds nothing, and a copy whose source is gone stays |
the hive (hive-c0re) |
✅ a changed source is written over the copy in the next pass | ✅ read every 5 minutes |
swarm/services/attic/jwt-secret |
swarm-bao-attic-secret on the cache's host, when none is stored |
swarm-bao-attic-secret, swarm-bao-attic-tokens |
❌ a new key invalidates both tokens | ❌ atticd restarts when the file changes |
swarm/services/ci-metrics/forge/token |
swarm-controller, at start and every 5 minutes |
swarm-ci-metrics, swarm-controller |
✅ when missing or no longer matching the forge | ✅ read on every pass |
| (not in the store) a hive's mTLS leaf | the store's PKI, or you by hand | the hive itself, from disk | must be stated | must be stated |
Operating notes
The name main belongs to the swarm's own homeserver account, which the
controller mints, so the controller rejects any account you link under it.
swarm-controller checks every forge token itself: it looks at every agent
that has a store identity at start and every five minutes. For any whose
stored token is missing or stale it creates the forge user if there isn't one,
then mints the token. To check one agent now:
swarmctl agent mint-forge-token <agent>
An agent without a store identity gets no swarm token and keeps using the
forge-token file in its state dir, if it has one; re-create it with
swarmctl agent create to give it one. The swarm leaves an agent it already
declares as it is. For an agent with no declaration, such as one only its hive
knows about, agent create declares it paused; set it up from its card in
the swarm UI.
⚠️ Re-create existing agents before deploying a hive-side change that makes a
container require a credential it may not have. A container whose store
identity is absent doesn't start, so create the identities first, setting any
newly paused agent up as above.
Revoking an agent's queue credential. Declaring an agent destroyed —
PUT /api/hives/{hive}/agents/{agent}/state — deletes
swarm/agents/<agent>/queue and every version it held. An open queue
connection closes within an hour. An agent recreated under the same name gets a
fresh secret. It leaves the agent's mTLS leaf alone. If the store refuses the
delete, swarm-controller logs an error naming the agent and the teardown
continues; declare the agent destroyed again to re-run the revocation.
A failed store login retries every 30 seconds for a day. When a later attempt
lands a changed value, the service that loaded it restarts. If the fetch unit
can't compare file times it logs cannot compare the mtime of <path> … — leaving <unit> as it is at err, and the service keeps the old value until
the next timer tick.
Where each secret lives on disk
One row per secret, by the place that owns it: who generates it, where the file
or store path is, and what an operator of a hive elsewhere places. How the
pieces fit, and the reasoning behind them, is in credentials.md.
Swarm-level — one of each per swarm
| secret | generated by | lives at | hive elsewhere |
|---|---|---|---|
| swarm-services root CA (cert + key) | swarm-bao.nix's granting unit, inside the store's pki mount |
the key never leaves the mount; the cert lands at /var/lib/swarm-bao-services-pki/services-root.pem (0644) and at /var/lib/hive-tls/swarm-services-root.pem on every hive |
nothing to copy — every hive receives the cert with the leaf it asks the store for, or from pki/ca/pem where it fronts no swarm name and needs a store identity |
| authelia session, JWT, storage-encryption and OIDC HMAC keys | authelia-swarm-secrets in authelia's container. It stores each in the swarm secret store at swarm/services/swarm-authelia/keys/<name> when the store holds none: the value of the key an earlier release generated, or a new one when there is no such file. The files stay on disk. While the store's policy doesn't grant the leaf yet, or the store is down, the unit runs on the files. A container with no secret publisher identity generates the files, as before |
/var/lib/authelia-swarm/{session,jwt,storage-encryption,oidc-hmac}.key |
nothing to place: the publisher's store leaf is the one in the row below. A store that stays unreachable on a start with no key file keeps authelia from starting |
| authelia OIDC issuer key (RSA) | same unit, same store path (oidc-issuer) |
/var/lib/authelia-swarm/oidc-issuer.key |
same — relying parties verify against the public half at /jwks.json |
swarm appservice registration (as_token, hs_token) |
swarm-matrix-ctl appservice sync in hive-matrix. The store's pair wins; a store holding none takes the container's own tokens, so the value doesn't change by moving. Only appservice render on a container with neither mints a new pair |
/var/lib/swarm-matrix-appservice/{as-token,hs-token,swarm.yaml} in hive-matrix, the last copy sync took, which tuwunel starts from; the store path is swarm/controller/swarm-controller/matrix/appservice-registration |
nothing to place: matrix-ctl's store leaf is the one the container binds. A store that's down leaves the homeserver running on the files |
| OIDC client secret, plaintext half | authelia crypto hash generate --random |
/var/lib/authelia-swarm/oidc-clients/<id>.secret |
operator provides the file and names it in whichever option reads it — sso.clientSecretFile for a service; the forge and, by default, the hive collector need none — swarm-bao-forge-oidc and swarm-bao-hive-otel-oidc fetch it from the store (how) |
| OIDC client secret, digest half | the same mint | oidc-clients/<id>.digest |
authelia's own half; merged at runtime via settingsFiles |
| the swarm collector's copy of its OIDC secret | swarm-bao-otel-oidc.service reads it out of the swarm secret store, on every host that runs the collector and holds a store identity |
/run/swarm-otel-oidc/<id>.secret on the host's tmpfs, bind-mounted read-only into the swarm-otel container — never on persistent disk, so a reboot fetches it again |
same unit, same path — one route, co-located or not. A collector with no store identity has clientSecretFile == null, its already-supported unauthenticated-push degrade — see swarm-level secrets |
| Grafana's copy of its OIDC secret | swarm-bao-grafana-oidc.service reads it out of the swarm secret store, on every host that runs Grafana |
/run/swarm-grafana-secrets/<id>.secret on the host's tmpfs, bind-mounted read-only into the swarm-grafana container — never on persistent disk, so a reboot fetches it again |
same unit, same path — one route, co-located or not. Nothing for an operator to place beyond this host's store leaf, see hive-level secrets |
| Grafana's admin password | swarm-bao-grafana-admin.service creates it in the swarm secret store at swarm/services/<grafana client id>/admin/password when the store holds none, then copies it out, on every host that runs Grafana. Grafana's leaf may create that leaf but not update it, so the store keeps the first value, and the secret publisher can't write it. To rotate, run bao kv metadata delete secret/swarm/services/<grafana client id>/admin/password with a token that may, such as the root token (Grafana's, the publisher's and the controller's can't), then systemctl restart swarm-bao-grafana-admin; a plain bao kv delete isn't enough, see the callout xkey's section |
/run/swarm-grafana-secrets/admin-password on the host's tmpfs, bind-mounted read-only into the swarm-grafana container, never on persistent disk: security.admin_password for a new database, and grafana cli admin reset-admin-password in grafana.service's preStart for an existing one, on every start |
same unit, same path. After a reboot the unit fetches the file again, and Grafana doesn't start until it exists, so it stays down rather than falling back to the upstream default admin |
| Grafana's metrics bearer | swarm-bao-grafana-metrics-bearer.service creates it in the swarm secret store at swarm/services/<grafana client id>/metrics/bearer when the store holds none, then copies it out, on every host that runs Grafana. Same grant as the admin password, create without update. To rotate, run bao kv metadata delete secret/swarm/services/<grafana client id>/metrics/bearer with a token that may, then systemctl restart swarm-bao-grafana-metrics-bearer |
/run/swarm-grafana-secrets/metrics-bearer on the host's tmpfs, bind-mounted read-only into the swarm-grafana container, holding Bearer <value>: the metrics datasource's Authorization header. The logs datasource sends the same header. swarm-metrics-auth on the metrics store's host, and a second instance on the log store's host (swarm-logs-grafana-auth), read the stored value themselves, each under its own leaf |
same unit, same path. The unit restarts Grafana when the file changes |
| Grafana's datasource encryption key | swarm-bao-grafana-secret-key.service stores it in the swarm secret store at swarm/services/<grafana client id>/secret-key when the store holds none, then copies it out, on every host that runs Grafana. It stores the key an earlier release generated at /var/lib/grafana-secret/secret_key in the container, so the key doesn't change, and mints a new one only on a host with no such file. Same grant as the admin password, create without update. The earlier file stays on disk. While the store's policy doesn't grant the leaf yet, or the store is down, the unit runs Grafana on that file |
/run/swarm-grafana-secrets/secret-key on the host's tmpfs, bind-mounted read-only into the swarm-grafana container: security.secret_key |
same unit, same path. Grafana doesn't start until the file exists. Changing the key leaves Grafana unable to read the datasource secrets it stored, so never delete the store path to rotate it |
| the swarm controller's webhook secret | swarm-controller stores it in the swarm secret store at swarm/controller/swarm-controller/forge/webhook-secret when the store holds none: the value of webhook-secret in its state directory, or a new one when there is no such file. The state directory file stays on disk and is the fallback while the store is unreachable or its policy doesn't grant the path yet |
the swarm secret store; the controller holds it in memory and registers the same value with the forge's hooks | nothing to place: the controller's store leaf already exists. A store that stays unreachable on a start with no state directory file leaves the webhook endpoint answering 503 |
| the metrics store responder's own store leaf (cert + key) | signLeaf in glue-bao-tls.nix (swarm-bao-pki.service), on the host that runs the secret store, once, subject deploy.bao.metricsAuthCommonName. swarm-bao-metrics-auth-policy.service writes the swarm-metrics-auth role and its read grants on the bearer leaf and on the metrics store forwarder's client secret, nothing else |
/var/lib/swarm-bao-pki/metrics-auth.pem and metrics-auth-key.pem on the store's host, handed to swarm-metrics-auth through LoadCredential (paths in argv, never values) and read by path by swarm-victoriametrics-forwarder-oidc |
the operator places a leaf with that subject on the metrics store's host and names it in deploy.victoriametrics.authBaoClientCertFile / authBaoClientKeyFile; evaluation fails naming both until then. If the store is on another host, deploy that host first: the role must exist before the responder's login succeeds, and until it does the gateway refuses every Grafana query (fail closed) |
| the secret publisher's store leaf (cert + key) | signLeaf in glue-bao-tls.nix (swarm-bao-pki.service), on the host that runs the secret store, once, subject deploy.bao.secretPublisherCommonName. swarm-bao-secret-publisher-policy.service writes the swarm-secret-publisher role: create and update under swarm/hives/*, swarm/services/* and the controller's client, and read on authelia's forwarder's client secret and authelia's five keys alone |
/var/lib/swarm-bao-pki/secret-publisher.pem and secret-publisher-key.pem on the host, handed into authelia's container as nspawn system credentials, which swarm-secret-publish and the forwarder's swarm-journal-forwarder-oidc load with LoadCredential. nspawn won't start authelia's container while a named file is missing |
the operator copies the leaf to authelia's host, beside the hive's own store leaf, where glue-secret-publisher-bao-identity.nix looks, or names it in deploy.swarm-secret-publisher.baoClientCertFile / baoClientKeyFile |
| the swarm controller's copy of its OIDC secret | swarm-controller reads it out of the swarm secret store at start, under its own leaf |
the daemon's memory only; never on disk | same read, co-located or not. Nothing for an operator to place beyond the controller's store leaf (deploy.swarm-controller.baoClientCertFile) |
| a hive's copy of its own OIDC secret | hive-c0re reads it out of the swarm secret store when it first connects to the queue, under the hive's own leaf | the daemon's memory only; never on disk | same read, co-located or not. Nothing for an operator to place beyond the hive's store leaf (deploy.bao.clientCertFile) |
| the hive collector's copy of its hive's OIDC secret | swarm-bao-hive-otel-oidc.service reads it out of the swarm secret store under the hive's own leaf, on every hive that runs the collector and holds that leaf; the leaf is the source |
/run/hive-otel-oidc/hive-<name>.secret on the hive's host tmpfs, 0400 root, loaded by LoadCredential — never on persistent disk, so a reboot fetches it again |
same unit, same path, co-located or not. Nothing for an operator to place beyond the hive's store leaf (deploy.bao.clientCertFile). Naming a secret file with otel.clientSecretFile instead is legacy for a host without a leaf: the build warns, and the fix is to place the leaf from /var/lib/swarm-bao-pki/hives/<hive>/ and drop the setting |
| the swarm controller's forge token | the forge's forgejo-swarm-controller-account, in-container, once |
the swarm secret store at swarm/controller/swarm-controller/forge/token, written by the mint under the forge publisher's leaf, which the forge container loads as a credential; the controller holds it in memory only. /var/lib/forgejo/swarm-controller-token-name in the forge container holds its name, the minter's record that it has minted |
nothing to place beyond the controller's store leaf. A forge host without the store needs the publisher's leaf placed (deploy.bao.forgePublisherClientCertFile) |
| the swarm controller's Grafana token | swarm-bao-grafana-controller-token.service, on the host that runs Grafana, as Grafana's built-in admin over its socket: it creates the swarm-controller service account as a Viewer (and sets it back to Viewer on every run), mints a token for it and writes it to the swarm secret store at swarm/controller/swarm-controller/grafana/token under Grafana's leaf. Re-run every ten minutes: it keeps a stored token Grafana accepts, and replaces one Grafana refuses with 401 (or a missing one), deleting every other token of that account first |
the swarm secret store only; the controller reads it for GET /api/alerts and holds it in memory, re-reading the store once when Grafana answers 401 |
nothing to place: Grafana's leaf is already required to run Grafana. Until the unit has run, /api/alerts answers 503 naming the empty path |
an agent's API-key environment (ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, an ACP provider key) |
hive-agent-backend-env.service, in the agent's container, on an agent with a store that uses useApiKey or the acp runtime. When the store holds nothing at swarm/agents/<agent>/backend-env and backendEnvironmentFile is a non-empty file, it creates the leaf with the file's exact bytes and -cas=0, under the agent's own leaf, which may create that path and not update it. It never rotates a value and never rewrites or deletes the file. To set or replace a key, use set the API-key environment in the swarm UI (PUT /api/hives/{hive}/agents/{agent}/backend-env): swarm-controller overwrites the leaf with its own grant, and the store's value then wins over the file. Its remove (DELETE on the same path) deletes the leaf, and an agent whose file has content then creates it from that file again |
the swarm secret store at that path; the unit writes it to /run/hive-agent-backend-env/env (tmpfs, 0600, root), and hive-agent and hive-subagent-daemon load that after backendEnvironmentFile as EnvironmentFile= |
the same unit, same path. A variable in the store wins over the same one in the file. With the store down at a start, the file alone carries the agent, as it did before the store held a copy; with an older hive build the agent ignores the leaf and keeps reading the file |
an agent's claude login (.credentials.json) |
swarm-claude-login.service, in its container, when an operator completes claude auth login from the agent's card in the swarm UI; it stores the file as a new version at swarm/agents/<agent>/claude-credentials under its own leaf, which may create and update that path and read nothing under an agent |
the swarm secret store at that path; the agent's hive-agent reads it under the agent's own leaf every 30 seconds while it waits for a login and writes it to ~/.claude/.credentials.json |
the same read, co-located or not; nothing for an operator to place beyond the agent's store leaf. When the store's copy and the agent's differ the KV version decides: the agent installs only a version newer than the last it installed (claude-credentials-version under the harness dir), because claude rotates the refresh token in its own copy and the stored one goes stale. The agent ignores a stale or equal version, and a relogin writes a newer one — claude-login.md |
| the queue responder's copy of its OIDC secret | the responder reads it out of the swarm secret store itself as it starts, under its own leaf, and again when authelia refuses it | the daemon's memory only; never on disk | same read, wherever authelia runs. Nothing for an operator to place beyond the responder's store leaf (deploy.nats.authBaoClientCertFile) |
| the queue container's journal forwarder's copy of the queue's OIDC secret | swarm-journal-forwarder-oidc.service inside the swarm-nats container reads it out of the swarm secret store under the responder's store leaf, and again every 10 minutes |
/run/swarm-journal-forwarder-oidc/client-secret on the container's tmpfs, root 0400, never on persistent disk; LoadCredential hands it to the collector |
same unit, same path. After a reboot the collector restarts until the file exists, then resumes from its saved journal cursor. Nothing for an operator to place beyond the responder's store leaf (deploy.nats.authBaoClientCertFile) |
| the CI runner, Grafana, log store, cache, forge and homeserver containers' journal forwarders' OIDC secrets | swarm-journal-forwarder-oidc.service inside each container reads its forwarder client's secret (swarm.<service>.otel.clientId) out of the swarm secret store under the store leaf the host already holds for that service, handed in as a system credential (bound in read-only, for the homeserver's), and again every 10 minutes |
/run/swarm-journal-forwarder-oidc/client-secret on the container's tmpfs, root 0400, never on persistent disk; LoadCredential hands it to the collector |
same unit, same path. Nothing for an operator to place beyond those leaves (deploy.bao.ciRunnerClientCertFile, deploy.bao.grafanaOidcClientCertFile, deploy.victorialogs.authBaoClientCertFile, deploy.bao.forgePublisherClientCertFile, deploy.bao.atticClientCertFile, deploy.matrix.ctlBaoClientCertFile) |
| the metrics store container's journal forwarder's OIDC secret | swarm-victoriametrics-forwarder-oidc.service on the store's host reads its forwarder client's secret (swarm.victoriametrics.otel.clientId) out of the swarm secret store under the metrics store responder's leaf, and again every 10 minutes: the container has no network to reach the store |
/run/swarm-victoriametrics-forwarder-oidc/<id>.secret on the host (tmpfs), root 0400, never on persistent disk; bound into the container read-only, where LoadCredential hands it to the collector |
same unit, same path. Nothing for an operator to place beyond that leaf (deploy.victoriametrics.authBaoClientCertFile) |
| the CI run poller's forge token | swarm-controller, with the forge's admin API, for its ci-metrics account, at start and in a five-minute pass |
the swarm secret store at swarm/services/ci-metrics/forge/token; swarm-ci-metrics reads it once per pass under its own leaf and holds it in memory for that pass. When the store's token no longer matches the forge's, the controller's next pass mints a new one; until then the poller's forge calls answer 401, the unit fails and no cursor moves |
nothing to place beyond the poller's leaf (deploy.bao.ciMetricsClientCertFile), on a host without the store |
| the CI run poller's copy of its OIDC secret | authelia; swarm-secret-publish copies it into the swarm secret store |
swarm-ci-metrics reads swarm/services/<swarm.ci-metrics.clientId>/oidc/client once per pass under its own leaf, and again when authelia refuses it; memory only. While it differs from authelia's, the token request fails, nothing gets pushed and no cursor moves |
same leaf, nothing else |
| authelia's container's journal forwarder's OIDC secret | authelia's mint, in its container, like every client's plaintext half (swarm.authelia.otel.clientId). swarm-secret-publish copies it to swarm/services/<id>/oidc/client, and swarm-journal-forwarder-oidc.service in the same container reads it back under the secret publisher's leaf, at start and every 10 minutes. The two copies can differ while the publisher hasn't caught up, for example while the store is down after a re-mint. Authelia then refuses the forwarder's token request, and the collector retries until the next read lands the new value and restarts it |
/run/swarm-journal-forwarder-oidc/client-secret on the container's tmpfs, root 0400; LoadCredential hands it to the collector. The mint's own copy stays in /var/lib/authelia-swarm/oidc-clients/<id>.secret |
none: the forwarder runs only in authelia's container, and is off there without the publisher's leaf |
| authelia subject store | swarmctl and swarm-authelia-bridge |
users.yml — one file, read and written by both |
swarmctl, on the host that runs authelia |
| wireguard private key | the operator — wg genkey |
whatever deploy.wireguard.privateKeyFile names |
always operator-provided; nothing generates this for you |
| queue auth-callout nkeys (user seed + account seed) | swarm-nats-auth ensure-callout-keys, run by swarm-bao-nats-callout-xkey on the queue's host when the store holds no seed; the verb stores a seed it finds at /var/lib/swarm-nats-callout/{callout-user,issuer}.seed as is, then removes the file |
the swarm secret store: the seeds at swarm/services/swarm-nats/callout/{user,issuer}-seed, read by the swarm-nats-auth role alone; the public halves at swarm/public/swarm-nats/callout-{user,issuer} |
nothing to place beyond the responder's store leaf (deploy.nats.authBaoClientCertFile) |
| queue auth-callout xkey (curve seed) | swarm-nats-auth ensure-callout-keys, run by swarm-bao-nats-callout-xkey on the queue's host when the store holds no seed |
the swarm secret store: the seed at swarm/services/swarm-nats/callout/xkey-seed, read by the swarm-nats-auth role alone; the public half at swarm/public/swarm-nats/callout-xkey |
nothing to place beyond the responder's store leaf (deploy.nats.authBaoClientCertFile) |
the queue's TLS key, copied for the gateway's /metrics vhost |
swarm-nats-gateway-cert.service on the queue's host, where the gateway uses self-signed TLS: a copy of the queue's bao-PKI TLS leaf (/var/lib/swarm-nats-tls/) once swarm-bao-nats-tls has issued it, else a throwaway 30-day self-signed pair for the queue's name until then |
/var/lib/swarm-nats-gateway-tls/{cert,key}.pem; the key is 0640 group nginx, read by nginx for the queue's /metrics vhost. The queue's own key stays 0600 root |
the copy is redone on each leaf renewal (swarm-bao-nats-tls starts the unit, which queues a non-blocking nginx reload when nginx is running); a restart before any leaf exists keeps the pair already there. Until swarm-bao-nats-tls issues the leaf the vhost serves the throwaway cert, so the collector's scrape fails TLS. Nothing for an operator to place |
| the secret store's own contents | openbao, on first bao operator init — an operator action, not a unit |
inside the swarm-bao container, at its own /var/lib/openbao, kept across rebuilds by ephemeral = false. ⚠️ Not a host path: nixos-container destroy swarm-bao takes the raft data with it, so back up the container's tree, not /var/lib/. Only the store's TLS material (/var/lib/swarm-bao-tls) and its PKCS11 token (/var/lib/swarm-bao-token) are host-level |
n/a — there is one store; a hive elsewhere is a client of it and holds none of this |
| the secret store's unseal material | the HSM/TPM under deploy.bao.seal = "pkcs11"; openbao itself under "shamir" |
in the token; or held by whoever ran bao operator init, which is what "shamir" means and why it's stated rather than inferred |
n/a — only the host running the store seals anything |
Hive-level — one of each per hive
| secret | generated by | lives at |
|---|---|---|
| hive leaf certs | hive-tls.nix, issued by the secret store's pki mount |
<deploy.hive-controller.tls.stateDir>/<name>.pem |
| the hive's own store identity | glue-bao-tls.nix on the store's host; placed by hand on every other host (ca.md) |
deploy.hive-controller.tls.baoHiveIdentityCertFile / baoHiveIdentityKeyFile |
| the cache's public key and pull token, for hive-priv | swarm-controller copies them to swarm/hives/<hive>/attic/{public-key,pull-token} (see the cache-facts row above) |
hive-priv-cache-facts (root) reads those two under the hive's own store identity every ten minutes and writes /run/hyperhive-priv/attic/public-key and netrc (tmpfs, root, each 0600, directory 0700); the cache's address comes from services.hyperhive.swarm.attic.domain and cache, not from the store. A store that holds no facts, or that doesn't answer, leaves the files as they stand, and a hive that never got them has hive-priv answer not available. |
| the forge's copy of its OIDC secret | swarm-bao-forge-oidc.service reads it out of the swarm secret store under the forge publisher's leaf, wherever authelia runs |
/run/hive-forge-oidc/<id>.secret on the host (tmpfs), bound into the forge container, handed to forgejo-sso-source through LoadCredential |
| the homeserver's copy of its OIDC secret | swarm-bao-matrix-oidc.service reads it out of the swarm secret store under matrix-ctl's leaf, wherever authelia runs |
/run/hive-matrix-oidc/<id>.secret on the host (tmpfs), bound into the homeserver container, handed to tuwunel through LoadCredential |
| this hive's forge read token | swarm-controller, into the secret store at swarm/hives/<hive>/forge/token, for the hive-<hive> account; a pass at start and every five minutes re-mints a missing or stale token |
hive-c0re reads it under the hive's own store identity, and copies it (account on the first line, token on the second) to /run/hyperhive/forge-hive-token (tmpfs, 0600 hive-core) for nix's git, which hive-priv also runs. Each internal/meta read and each boot rewrites the copy when it differs; where they differ after a rotation, a build that starts before the next read fails to authenticate, and a retry reads the new token |
| squid's users file | hive-c0re (workers::squid_users), every 30 seconds, from the proxy credentials of its agents and the self-check |
services.hyperhive.squid.usersFile (/var/lib/hive-squid/users, hive-core:squid, 0640): <user>:<hash> lines, SHA-512 crypt hashes only, renamed into place when it changed. unrestricted beside it lists the users whose agent holds the swarm's unrestricted_internet flag, names only; it lists every agent the hive never read as off systemd-tmpfiles creates it empty so squid's helper never sees a missing file. A hive whose agents have no credential yet (an older controller) has an empty file: every login fails and the self-check reports ok=false; no agent sends a credential, so no traffic changes |
| the squid self-check's password | swarm-controller mints swarm/hives/<hive>/squid-selfcheck; squid-selfcheck-credential (root, every 10 minutes) reads it as the hive |
/run/credstore/hive-squid.selfcheck (tmpfs, root, 0400), imported by squid-selfcheck.service as a systemd credential and handed to curl on its stdin. Both self-check users (selfcheck.squid and the always-unrestricted selfcheck-unrestricted.squid) use it, so whoever holds it has unrestricted egress through this squid Missing, or different from the users file: the self-check logs credential=absent or a 407 and fails, and nothing else breaks |
Store identities, one per principal
Bao matches a cert-auth role on the certificate's subject, so a certificate is an identity and sharing one merges the identities. Each of these leaves has a role and a policy of its own, and reaches one path, except the review classifier's, the CI runner's, the builder's, the cache's, the CI run poller's, the claude login minter's, the forge host's and Grafana's, which reach more than one:
| unit | option pair under deploy.bao. |
reads, or writes |
|---|---|---|
swarm-bao-grafana-oidc, swarm-bao-grafana-admin, swarm-bao-grafana-secret-key, and Grafana's journal forwarder in its container |
grafanaOidcClientCertFile / grafanaOidcClientKeyFile |
reads swarm/services/<grafana client id>/oidc/client and swarm/services/<grafana forwarder client id>/oidc/client; creates and reads swarm/services/<grafana client id>/admin/password and swarm/services/<grafana client id>/secret-key |
swarm-bao-otel-oidc |
otelOidcClientCertFile / otelOidcClientKeyFile |
swarm/services/<collector client id>/oidc/client |
swarm-bao-forwarder-oidc |
forwarderOidcClientCertFile / forwarderOidcClientKeyFile |
swarm/services/<store forwarder client id>/oidc/client |
swarm-bao-ci-runner, and the runner's journal forwarder in its container |
ciRunnerClientCertFile / ciRunnerClientKeyFile |
swarm/services/ci-runner/registration-token and swarm/services/<runner forwarder client id>/oidc/client |
swarm-review-classifier, in its container |
reviewClassifierClientCertFile / reviewClassifierClientKeyFile |
swarm/services/review-classifier/anthropic-api-key, swarm/services/review-classifier/acp-provider and swarm/services/<classifier client id>/oidc/client |
swarm-claude-login, in its container |
claudeLoginClientCertFile / claudeLoginClientKeyFile |
creates and updates swarm/agents/+/claude-credentials; reads swarm/services/<minter client id>/oidc/client |
swarm-notify-router, in its container |
notifyRouterClientCertFile / notifyRouterClientKeyFile |
swarm/services/<router client id>/oidc/client |
swarm-builder, in its container |
builderClientCertFile / builderClientKeyFile |
swarm/services/builder/forge/token, swarm/services/attic/push-token and swarm/services/<builder client id>/oidc/client |
swarm-bao-attic-secret and swarm-bao-attic-tokens |
atticClientCertFile / atticClientKeyFile |
creates and reads swarm/services/attic/jwt-secret, swarm/services/attic/push-token and swarm/services/attic/pull-token |
swarm-ci-metrics |
ciMetricsClientCertFile / ciMetricsClientKeyFile |
swarm/services/ci-metrics/forge/token and swarm/services/<poller client id>/oidc/client |
swarm-bao-forge-oidc and the forge's controller-token mint and its journal forwarder in its container |
forgePublisherClientCertFile / forgePublisherClientKeyFile |
writes swarm/controller/swarm-controller/forge/token; reads swarm/services/<forge client id>/oidc/client and swarm/services/<forge forwarder client id>/oidc/client |
The option names are deploy.bao.<pair>; the common names, what a missing leaf
costs and how to place a leaf by hand are in
per-principal identities.