# Swarm CA A hive's internal TLS chains to a **swarm root CA**: the root signs each hive's own CA, and that hive CA signs the gateway leaf. A peer that trusts the root once validates every hive in the swarm, present and future, instead of being pinned to each one by hand. That is the whole point of the hierarchy — it turns per-peer trust from O(n²) hand-pinning into one anchor per swarm. ## Two provisioning modes, one structure What differs is who puts the artifacts on disk: | | swarm root | this hive's CA | | --- | --- | --- | | default | operator-provided | operator-provided, else self-signed as before | | `autoConfigure = true` (all on one host) | generated by `swarm-ca.service` on first boot | issued by `hive-tls-ca.service` under the root | `services.hyperhive.swarm.ca.autoConfigure` selects between them, and is **off by default**: a swarm's services and its hives can live on different hosts, and a host cannot tell whether it is the one holding the root, so setting the swarm CA up is an operator action rather than something a host assumes. Turn it on for an all-on-one-host deployment and the hierarchy costs no configuration. It defaults from `services.hyperhive.enableAllLocalDefaults`, the single switch that says "this box is the whole deployment". **A hive given neither artifact keeps the self-signed CA it has always had.** It serves TLS exactly as before and simply isn't part of a swarm's trust hierarchy — the right outcome for a hive nobody has federated yet. Only `autoConfigure` issues a hive sub-CA, because only that case can: signing one needs the root's private key. Moving the swarm CA onto its own host is then a matter of moving `services.hyperhive.swarm.ca.stateDir` and leaving `autoConfigure` off — there is no second code path to switch to. ### The warning about uncovered service names The swarm's service names (`swarm.forge.domain`, `swarm.matrix.gatewayHost`, `swarm.authelia.domain`) default to siblings of the hive domain — `forge.`, not `forge.`. The hive CA's leaf is a **single-label** wildcard over the hive's own domain, so it cannot cover them; only the swarm-services leaf can, and this host signs that one only under `autoConfigure`. A hive with neither serves the hive leaf on those names and every client sees a name mismatch. So `hive-tls` emits an eval-time **warning** naming the uncovered names. It is deliberately not an assertion: this module can see what *it* is able to issue, but not an operator-installed sub-CA in `swarm.ca.stateDir`, an external ACME setup, or a certificate delivered by any other means. If you have arranged one, the warning is expected and can be ignored. Otherwise either install the sub-CA, or pin the names back under the hive domain — a supported migration, since the sub-CA is constrained to the *configured* names and the swarm root carries no name constraints at all. ## Constraints on the material The root's private key never reaches the nix store: the store is world-readable and content-addressed, so a key committed to a flake is a key published to everyone who builds it. Only certificates are distributed. Each hive CA is **name-constrained** (X.509 `nameConstraints`) to that hive's own domain, so a hive CA that leaks can only mint names inside its own subdomain — enforced by every verifier rather than by convention. The constraint excludes both IP families as well, since a permitted-DNS-only constraint says nothing about IP SANs. The root is issued with `pathlen:1`: it may sign hive CAs, and those may sign leaves, and the chain stops there. ## What to hand a peer `hivectl peer-config` prints the `services.hyperhive.swarm.hives.""` block a peer operator pastes into their own config. When this hive's gateway serves a self-signed leaf under the hierarchy (detected by the presence of `/trust-bundle.pem`), it also prints a one-time `scp` line installing the **swarm root** — `/root.pem`, not this hive's own CA — on the peer's host: ``` scp /var/lib/swarm-ca/root.pem :/var/lib/swarm-ca/root.pem ``` That is the point of the hierarchy: the root is installed **once per swarm host**, not once per peer, so a hive joining later needs no edit on the hives already running. A hive whose cert already chains to a public CA has nothing to install — `peer-config` says so instead. Handing a peer this hive's own `ca.pem` would not work even as a one-off: once a hive CA is an intermediate under the swarm root, it is no longer something a verifier can build a chain *to* — OpenSSL will not terminate a chain at a trusted non-self-signed certificate without `-partial_chain`. That is why the tool distributes the root, not a per-hive file. ## Adopting the hierarchy on an existing hive A hive that predates the swarm root carries a self-signed `ca.pem`, and adopting the hierarchy means replacing it. That invalidates an anchor consumers already trust, and they refresh on their own schedule — agents only pick up new trust when their container restarts, peers only on their own rebuild. **Who is allowed to decide that is what splits the two cases.** ### Where this host owns the root (`autoConfigure`) Adoption happens by itself, once. `hive-tls-ca.service` notices that `ca.pem` does not chain to the root, keeps the old certificate as `ca-previous.pem`, and re-issues under the root; the next leaf is signed by the new CA. It is safe to automate here precisely because this is the all-on-one-host shape: every consumer is on this box, so "when will they have refreshed" is knowable rather than guessed. The old CA stays in `trust-bundle.pem` afterwards, so adoption is **additive to the anchor set before it is subtractive** — a container that has not restarted yet still validates. Removing `ca-previous.pem` is a deliberate later step: how long is long enough is a property of the deployment, not something the unit can know. A marker file (`.swarm-ca-adopted`) records that this ran. Its *absence* is the trigger, so adoption fires once per hive rather than being re-decided on every activation. ### Everywhere else No automatic adoption. `hive-tls-ca.service` fails, loudly, naming both certificates and giving the two-command recipe: ``` rm /ca.pem /ca-key.pem systemctl restart hive-tls-ca.service ``` Failing rather than warning is deliberate: a hive whose CA does not chain to the root it has been given is misconfigured, and a warning in a build log is not something anyone reads twice. To keep the current CA on purpose — a hive that deliberately stays outside the hierarchy, or one mid-migration — `touch` the marker file named in the message. That is a decision, and it is recorded as one. A hive with **no** root configured at all is not affected by any of this: it self-signs exactly as it always has. ## Distributing the root The root **key** is a runtime file for an obvious reason: the nix store is world-readable and content-addressed, so a key committed to a flake is a key *published* to every consumer of that flake. The root **certificate** is a runtime file as a *consequence* — it lives beside the key under `swarm.ca.stateDir` — and that has a cost worth naming, because it is not obvious and it bites at a distance: > **Nothing whose trust store is assembled at build time can reference > the swarm root.** `security.pki.certificateFiles` is read inside the > derivation; the root does not exist there. Two consumers, and only one of them is fine: - **Agents are covered.** `hive-tls.nix` folds the root into this hive's `trust-bundle.pem`, hive-c0re receives that path as `HIVE_TLS_CA_PATH`, and the meta-flake renderer embeds that one file next to every agent's flake. The bundle is the runtime-to-build-time bridge. - **The Matrix container crosses the same bridge**, via the shared `lib/hive-ca-trust.nix` helper that `hive-ci` and `hive-forge` already use: the bundle is bind-mounted read-only into the container, and a oneshot concatenates it with the system CAs before tuwunel starts. The consumption differs per runtime and is the part worth knowing. tuwunel links no openssl, which makes `SSL_CERT_FILE` look inapplicable — it isn't: its rustls-based TLS stack still resolves trust through the platform certificate store on Linux, and that store honors `SSL_CERT_FILE`, so the env var takes effect the same way it would for an OpenSSL-linked binary. > ⚠️ **Concatenate; never point `SSL_CERT_FILE` at the anchor alone.** > `openssl-probe` uses it *instead of* the default store, so naming > just the bundle would drop every public CA and break federation with > the wider matrix network — a much bigger outage than the one being > fixed. The same caveat applies to `hive-forge` (Go) for the same > reason. To put the root on another host, copy the certificate to the same path there (`scp /root.pem :/root.pem`). One anchor per host, not one per peer: a hive joining later needs no edit on the hives already running, which is the whole point of the hierarchy.