docs: clear write-good.Passive hits in swarm secrets/README docs
Part of #4548 — third batch. This PR: docs/swarm/secrets.md (16 hits), docs/swarm/README.md (15 hits) = 31 more of the 353 (130 total across the three PRs so far). Same method as #4551/#4553: minimal, meaning-preserving active-voice rewrites, no restructuring beyond what each flagged sentence needed, no vale-off silencing. Genuine false positives left in place: predicate-adjective state descriptions ("is required" x4, "is enabled", "is disabled"), one systemd terminology reference ("is wanted (not required)" — literally naming systemd's Wants=/Requires= dependency-type distinction, not passive voice), and one design-intent idiom ("both are expected" — same shape as earlier docs-lint work).
This commit is contained in:
parent
18c5b5c5ad
commit
c985702bb0
2 changed files with 55 additions and 55 deletions
|
|
@ -18,7 +18,7 @@ build .#docs-swarm` / `.#docs-deploy` or the website's `/options/swarm.html`
|
|||
own `services.hyperhive.domain` DNS name and its own set of agent
|
||||
containers.
|
||||
- **swarm** — one or more hives whose operators have declared them
|
||||
as peers. Agents can be qualified as `agent@hive-domain`.
|
||||
as peers. You can qualify an agent as `agent@hive-domain`.
|
||||
- **peer hive** — any hive in `services.hyperhive.swarm.hives` other
|
||||
than this one. Peers are _derived_, not declared: the directory lists
|
||||
every hive including yourself, and `hiveName` says which one you are.
|
||||
|
|
@ -134,7 +134,7 @@ because `services.hyperhive.hiveName` says which entry is itself.
|
|||
Empty (the default) means this host isn't in a swarm. Once non-empty it
|
||||
**must** contain an entry for `hiveName`; eval fails naming the missing
|
||||
hive. That assertion is load-bearing rather than pedantic — "my peers"
|
||||
is derived as _everything that isn't me_, so a directory that doesn't
|
||||
comes from _everything that isn't me_, so a directory that doesn't
|
||||
contain you derives every hive as a peer and you peer with yourself.
|
||||
|
||||
`domain` defaults to `<name>.<swarm.domain>`, the convention every hive
|
||||
|
|
@ -161,7 +161,7 @@ addressed by something else.
|
|||
directory and serves it at `GET /api/hives`; `swarm-ui`'s overview
|
||||
page renders it (`docs/swarm/ui.md`). This is the operator-facing
|
||||
"what hives exist" surface — a per-hive dashboard "peer hives"
|
||||
display existed here once and was removed in favour of it.
|
||||
display existed here once; it no longer exists, in favour of this.
|
||||
|
||||
2. **Matrix federation** — when `matrix.enable` is on, tuwunel
|
||||
federates with the peer's matrix server (discovered via the peer's
|
||||
|
|
@ -170,12 +170,12 @@ addressed by something else.
|
|||
**container's** trust bundle, independent of this directory.
|
||||
|
||||
⚠️ **That container currently trusts no swarm-internal CA**, so a
|
||||
self-signed gateway certificate doesn't federate. The swarm root
|
||||
can't be listed there: `security.pki.certificateFiles` is
|
||||
self-signed gateway certificate doesn't federate. You can't list the
|
||||
swarm root there: `security.pki.certificateFiles` is
|
||||
read when the system is _built_, and the root is a runtime file (its
|
||||
key must never enter the store), so there is no build-time name for
|
||||
it. Bridging that needs a runtime mechanism and is tracked as its own
|
||||
issue. Until then, federation needs CA-issued certs (ACME). See
|
||||
it. Bridging that needs a runtime mechanism; a separate issue tracks
|
||||
it. Until then, federation needs CA-issued certs (ACME). See
|
||||
`docs/integrations/matrix.md` for federation firewall + TLS requirements.
|
||||
|
||||
3. **WireGuard mesh** (optional) — `deploy.wireguard.enable` reads each
|
||||
|
|
@ -281,7 +281,7 @@ is required. Out of scope for v0.
|
|||
|
||||
## Snapshot store
|
||||
|
||||
One further option lives in this namespace but is documented with the
|
||||
One further option lives in this namespace but its docs live with the
|
||||
service it points at: `services.hyperhive.swarm.snapshotStore.{address,
|
||||
port}` tells this hive where the swarm's `btrfs receive` endpoint is, so
|
||||
`hivectl agent <name> subvol snapshot push` has somewhere to stream to.
|
||||
|
|
@ -296,8 +296,8 @@ migrating agent keeps one unbroken incremental chain. See
|
|||
`services.hyperhive.deploy.swarm-controller.enable` runs the `swarm-controller`
|
||||
daemon on this host. **Off by default and deliberately not derived from
|
||||
`services.hyperhive.enable`**: a swarm has one controller, so enabling it
|
||||
is a statement about swarm topology, not about whether hyperhive is
|
||||
installed. Every hive runs `hive-c0re` (the agents on that host); one
|
||||
is a statement about swarm topology, not about whether the operator has
|
||||
installed hyperhive. Every hive runs `hive-c0re` (the agents on that host); one
|
||||
hive additionally runs this (what's true across hives).
|
||||
|
||||
What it serves, why it's a unix socket rather than a port, and the
|
||||
|
|
@ -380,8 +380,8 @@ with the credential itself and aren't configurable.
|
|||
|
||||
On a host that runs the queue, `agentNatsUrl` defaults to
|
||||
`nats://<network.bridgeIp>:<swarm.nats.port>`, which is the only address that
|
||||
works from inside a container: the port is opened on the bridge interface and
|
||||
nowhere else. ⚠️ **Never a loopback address here** — the hive's own
|
||||
works from inside a container: the firewall opens the port on the bridge
|
||||
interface and nowhere else. ⚠️ **Never a loopback address here** — the hive's own
|
||||
`statusPublish.natsUrl` is loopback and correct, because `hive-c0re` shares the
|
||||
host's network namespace. An agent doesn't, so `127.0.0.1` reaches the agent.
|
||||
|
||||
|
|
@ -391,7 +391,7 @@ above, plus `HIVE_AGENT_OIDC_CLIENT_SECRET_FILE` and
|
|||
`HIVE_AGENT_OIDC_CLIENT_ID_FILE`, which point into the unit's own credentials
|
||||
directory. The last two come from the delivered credential rather than from
|
||||
config — see [`secrets.md`](secrets.md#hive-level--one-of-each-per-hive) for
|
||||
how it gets there. A hive that hasn't been given the queue's address for its
|
||||
how it gets there. A hive lacking the queue's address for its
|
||||
agents sets none of the four and each agent logs that it has none; a half-set
|
||||
environment logs an error and the harness keeps serving.
|
||||
|
||||
|
|
@ -404,10 +404,10 @@ agent talks about itself here and reads nothing. Rows aren't retained — a
|
|||
subscriber that wasn't listening missed them, the same as on the agent's own
|
||||
live stream.
|
||||
|
||||
A row too large for the queue's `max_payload` would be refused outright and
|
||||
The queue would refuse a row too large for its `max_payload` outright and
|
||||
take the connection down with it, so the harness drops such a row's body before
|
||||
sending and leaves a marker in its place; the summary, level and icon still
|
||||
arrive. A row that's too large even without its body is logged and skipped.
|
||||
arrive. The harness logs and skips a row that's too large even without its body.
|
||||
|
||||
The second thing an agent publishes is its **turn-state header**, on
|
||||
`$SWARM.agent-state.<hive>.<agent>` — same shape of subject, same grant
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Swarm secrets: what exists, and where each one lives
|
||||
|
||||
A swarm's credentials are generated in three different places and read in a
|
||||
fourth, so "where does this file go" has a different answer per deployment.
|
||||
A swarm generates its credentials in three different places and reads them
|
||||
in a fourth, so "where does this file go" has a different answer per deployment.
|
||||
This page is that answer, one row per secret.
|
||||
|
||||
Two rules run through all of it.
|
||||
|
|
@ -14,7 +14,7 @@ than a convenience.
|
|||
|
||||
The rule is about what must stay secret, not about credentials generally.
|
||||
**Public material is a value**: a certificate, or a public nkey like
|
||||
`deploy.nats.calloutUserPublicKey`, is published to every client that connects,
|
||||
`deploy.nats.calloutUserPublicKey`, reaches every client that connects,
|
||||
so the store is a perfectly good place for it.
|
||||
|
||||
**The generator and the reader typically live in different containers.** They share
|
||||
|
|
@ -33,31 +33,31 @@ say _where services run_, these say _who is responsible for a secret
|
|||
file being there_ --- the two lists don't line up one-to-one, and
|
||||
neither is a renaming of the other.
|
||||
|
||||
| topology | what it means | who places secrets |
|
||||
| ------------------ | ---------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| **all-local** | one host runs the swarm's shared services and its own hive | nobody — each secret is generated where it's read, or copied by a host unit |
|
||||
| **swarm-managed** | the swarm's services run on a host with `swarmctl` | `swarmctl` writes what it owns; the rest is still generated in place |
|
||||
| **hive elsewhere** | a hive that federates with a swarm it doesn't host | the operator provides the file and names it in config |
|
||||
| topology | what it means | who places secrets |
|
||||
| ------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| **all-local** | one host runs the swarm's shared services and its own hive | nobody — the reader itself generates each secret, or a host unit copies it |
|
||||
| **swarm-managed** | the swarm's services run on a host with `swarmctl` | `swarmctl` writes what it owns; the rest is still generated in place |
|
||||
| **hive elsewhere** | a hive that federates with a swarm it doesn't host | the operator provides the file and names it in config |
|
||||
|
||||
## Swarm-level — one of each per swarm
|
||||
|
||||
| secret | generated by | lives at | hive elsewhere |
|
||||
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| swarm root CA cert | `swarm-ca.nix` first-boot unit, when `autoConfigure` is set | `/var/lib/swarm-ca/root.pem` | operator copies the **cert** in; it's public |
|
||||
| swarm root CA key | same unit | `/var/lib/swarm-ca/root-key.pem`, `0600` | stays on whichever host holds it — see the constraint below |
|
||||
| swarm-services sub-CA (cert + key) | `swarm-ca.nix`, signed by the root | `/var/lib/swarm-ca/services-ca{,-key}.pem` | issued where the root lives |
|
||||
| authelia session, JWT and storage-encryption keys | authelia's first-boot unit, in-container | `/var/lib/authelia-swarm/{session,jwt,storage-encryption}.key` | generated in place; nothing outside that container reads them |
|
||||
| authelia OIDC HMAC key | same unit | `/var/lib/authelia-swarm/oidc-hmac.key` | same |
|
||||
| authelia OIDC issuer key (RSA) | same unit | `/var/lib/authelia-swarm/oidc-issuer.key` | same — relying parties verify against the **public** half at `/jwks.json` |
|
||||
| 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, `otel.clientSecretFile` for the hive's telemetry collector |
|
||||
| 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** | `/var/lib/swarm-otel-oidc/<id>.secret` inside the `swarm-otel` container | same unit, same path — one route, co-located or not. A collector with no store identity is left with `clientSecretFile == null`, its already-supported unauthenticated-push degrade — see below |
|
||||
| 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** | `/var/lib/grafana-oidc/<id>.secret` inside the `swarm-grafana` container | same unit, same path — one route, co-located or not. Nothing for an operator to place beyond this host's store leaf, see below |
|
||||
| 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-callout-keys` first-boot unit, when `deploy.nats.autoGenerateCallout` is set | `/var/lib/swarm-nats-callout/{callout-user,issuer}.seed`, `0600` | operator mints both with `nk` and names them in `deploy.nats.calloutUserSeedFile` / `deploy.nats.calloutIssuerSeedFile` |
|
||||
| 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 |
|
||||
| secret | generated by | lives at | hive elsewhere |
|
||||
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| swarm root CA cert | `swarm-ca.nix` first-boot unit, when the operator sets `autoConfigure` | `/var/lib/swarm-ca/root.pem` | operator copies the **cert** in; it's public |
|
||||
| swarm root CA key | same unit | `/var/lib/swarm-ca/root-key.pem`, `0600` | stays on whichever host holds it — see the constraint below |
|
||||
| swarm-services sub-CA (cert + key) | `swarm-ca.nix`, signed by the root | `/var/lib/swarm-ca/services-ca{,-key}.pem` | issued where the root lives |
|
||||
| authelia session, JWT and storage-encryption keys | authelia's first-boot unit, in-container | `/var/lib/authelia-swarm/{session,jwt,storage-encryption}.key` | generated in place; nothing outside that container reads them |
|
||||
| authelia OIDC HMAC key | same unit | `/var/lib/authelia-swarm/oidc-hmac.key` | same |
|
||||
| authelia OIDC issuer key (RSA) | same unit | `/var/lib/authelia-swarm/oidc-issuer.key` | same — relying parties verify against the **public** half at `/jwks.json` |
|
||||
| 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, `otel.clientSecretFile` for the hive's telemetry collector |
|
||||
| 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** | `/var/lib/swarm-otel-oidc/<id>.secret` inside the `swarm-otel` container | 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 below |
|
||||
| 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** | `/var/lib/grafana-oidc/<id>.secret` inside the `swarm-grafana` container | same unit, same path — one route, co-located or not. Nothing for an operator to place beyond this host's store leaf, see below |
|
||||
| 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-callout-keys` first-boot unit, when the operator sets `deploy.nats.autoGenerateCallout` | `/var/lib/swarm-nats-callout/{callout-user,issuer}.seed`, `0600` | operator mints both with `nk` and names them in `deploy.nats.calloutUserSeedFile` / `deploy.nats.calloutIssuerSeedFile` |
|
||||
| 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 |
|
||||
|
||||
Authelia mints the three keys for itself, in-container, precisely
|
||||
because nothing outside that container ever reads them. **That's the test worth
|
||||
|
|
@ -68,8 +68,8 @@ Two telemetry collectors exist, and they land on opposite sides of that test.
|
|||
|
||||
The **hive's** collector needs no delivery step. It authenticates to the swarm's
|
||||
collector as its own hive, and it's a host unit rather than a container, so on
|
||||
an all-local swarm it reads authelia's file where it lies and no second copy is
|
||||
made. On any other topology it's an ordinary "operator provides the file"
|
||||
an all-local swarm it reads authelia's file where it lies and nothing makes a
|
||||
second copy. On any other topology it's an ordinary "operator provides the file"
|
||||
case — see `services.hyperhive.otel.clientSecretFile`.
|
||||
|
||||
The **swarm's** collector does need one, and its delivery route is the fourth
|
||||
|
|
@ -77,8 +77,8 @@ store reader — the same shape as Grafana's, described in full below. In short:
|
|||
`swarm-bao-otel-oidc.service` reads `swarm/services/<client-id>/oidc/client`
|
||||
out of the store on every host that runs the collector and holds a store
|
||||
identity, whether authelia is a network away or in the container next door.
|
||||
The client it reads for is registered unconditionally, the same call
|
||||
`glue-grafana-oidc-client.nix` makes for Grafana's: authelia refuses a
|
||||
The same call `glue-grafana-oidc-client.nix` makes for Grafana's registers
|
||||
the client it reads for unconditionally: authelia refuses a
|
||||
bearer-authz client with no audience, and the push audiences supply one even
|
||||
for a collector with nothing published to scrape.
|
||||
|
||||
|
|
@ -90,9 +90,9 @@ there. `swarm-bao-otel-oidc.service` therefore doesn't render at all without
|
|||
`deploy.bao.clientCertFile` / `clientKeyFile`, the same choice
|
||||
`glue-matrix-bao-token.nix` and `glue-queue-agent-credential.nix` make for
|
||||
their own optional readers, rather than the hard assertion `swarm-grafana.nix`
|
||||
uses for its non-optional one. The secret can still be delivered by hand, at
|
||||
`services.hyperhive.deploy.swarm-otel.clientSecretFile`, on a collector this
|
||||
unit never reaches.
|
||||
uses for its non-optional one. The operator can still deliver the secret by
|
||||
hand, at `services.hyperhive.deploy.swarm-otel.clientSecretFile`, on a
|
||||
collector this unit never reaches.
|
||||
|
||||
### Minting the queue's callout nkeys
|
||||
|
||||
|
|
@ -178,8 +178,8 @@ Instead it keeps the value it minted under its `StateDirectory`,
|
|||
`/var/lib/swarm-secret-publish/matrix-appservice-token/<hive>` (`0700` dir,
|
||||
`0600` file), and mints only when that file is missing or empty. The `put`
|
||||
itself runs every time: re-putting the same bytes changes nothing for any
|
||||
reader, while a mint whose publish failed must not be left as a token this
|
||||
host holds and no hive can reach.
|
||||
reader, while a mint whose publish failed must not leave behind a token
|
||||
this host holds and no hive can reach.
|
||||
|
||||
Lose the state directory and the next run mints once more and republishes.
|
||||
That rotates the token, which readers pick up on their next start — nothing
|
||||
|
|
@ -187,7 +187,7 @@ that already registered breaks, because the token authenticates the
|
|||
appservice rather than living inside any account it created.
|
||||
|
||||
The path was `…/matrix/registration-token` while the homeserver still took a
|
||||
shared registration secret; a value left at the old path is read by nothing.
|
||||
shared registration secret; nothing reads a value left at the old path.
|
||||
|
||||
The **second reader** is the agent containers' queue credential:
|
||||
`glue-queue-agent-credential.nix` lands it as two files, the client secret and
|
||||
|
|
@ -266,9 +266,9 @@ as it does for the other readers: a store that says "not here" leaves the
|
|||
file alone and says so, since there is no such thing as a locally valid OIDC
|
||||
client secret to stand in.
|
||||
|
||||
⚠️ **Service↔store mTLS is its own trust domain.** A credential you must
|
||||
already hold to authenticate can't be fetched from the thing it authenticates
|
||||
you to, so the store's identity can't come from an authority the store
|
||||
⚠️ **Service↔store mTLS is its own trust domain.** You can't fetch a
|
||||
credential you must already hold to authenticate from the thing it
|
||||
authenticates you to, so the store's identity can't come from an authority the store
|
||||
distributes — which excludes the hive CA and the swarm CA both, and has nothing
|
||||
to do with the gateway's HTTPS certificates either way. `glue-bao-tls.nix`
|
||||
mints a CA that signs exactly two things, the store's server certificate and a
|
||||
|
|
@ -318,8 +318,8 @@ Whatever holds the swarm root is therefore what makes swarm-service certificates
|
|||
possible at all. Two things follow:
|
||||
|
||||
- **The root's private key is a runtime file and must never enter the nix
|
||||
store**, so nothing build-time can name it — `security.pki.certificateFiles` is
|
||||
read when the system is built, and is the wrong tool here. Trust reaches
|
||||
store**, so nothing build-time can name it — `security.pki.certificateFiles`
|
||||
reads at system build time, and is the wrong tool here. Trust reaches
|
||||
containers through a bind-mounted bundle assembled at boot instead.
|
||||
- **On any topology other than all-local, placing that key is an operations
|
||||
decision**, not something this module tree makes for you. A hive that hosts no
|
||||
|
|
|
|||
Loading…
Reference in a new issue