From daa6eb96f84866236971f31072fdd942fe03c411 Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 30 Aug 2026 20:18:54 +0200 Subject: [PATCH] deploy: move the hive CA's knobs to deploy.hive-controller.tls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `services.hyperhive.tls.{stateDir,caValidityDays,leafValidityDays}` sat at the top of `services.hyperhive`, which is meant to be everything about hyperhive rather than the settings of one hive. Where the hive CA lives, how long it lasts and how long the leaves it signs last are decisions of the host holding the key — `deploy.*`, by the same rule as the switches that moved before them. `hive-controller` is hive-c0re's new name (mara on the issue), so the knobs hang off the daemon that owns the CA rather than off a bare `tls` at the root. mkRenamedOptionModule entries carry existing configs. ⚠️ Unlike the two switch renames, these names are NOT unique, so this was swept by ALIAS BINDING rather than by identifier: hive-tls.nix alone holds two options spelled `stateDir` — its own `cfg.stateDir` and the swarm CA's `swarmCaCfg.stateDir`, four sites that must not move. Nine files bind an alias to this config; the rename followed those bindings. Two sites were invisible to the obvious check, and an unanchored sweep for `hyperhive\.tls\b` is what found them: the option declaration (`= {` after the path, so no trailing `.` or `;`) and the alias convention documented in a comment in lib/hive-ca-trust.nix. Also renamed the `` shorthand in four docs and two Rust doc comments, anchored on its delimiters — the new path contains the old one as a substring, so an unanchored replace would have doubled the prefix. --- docs/gateway.md | 2 +- docs/swarm/ca.md | 5 +++-- docs/swarm/secrets.md | 4 ++-- hive-c0re/src/meta.rs | 5 +++-- hivectl/src/wg.rs | 3 ++- nix/host-modules/deploy.nix | 19 +++++++++++++++++++ nix/host-modules/hive-c0re/default.nix | 2 +- nix/host-modules/hive-ci.nix | 2 +- nix/host-modules/hive-forge/default.nix | 2 +- nix/host-modules/hive-gateway/default.nix | 16 ++++++++-------- nix/host-modules/hive-matrix.nix | 2 +- nix/host-modules/hive-tls.nix | 4 ++-- nix/host-modules/lib/hive-ca-trust.nix | 2 +- nix/host-modules/otel.nix | 2 +- nix/host-modules/swarm-bao.nix | 4 ++-- nix/host-modules/swarm-ca.nix | 10 +++++----- nix/host-modules/swarm-controller.nix | 2 +- nix/host-modules/swarm-nats.nix | 2 +- 18 files changed, 55 insertions(+), 33 deletions(-) diff --git a/docs/gateway.md b/docs/gateway.md index 6d306c7f..91c4861e 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -204,7 +204,7 @@ Mutual exclusion: `tls.certDir` set together with `tls.acme.enable = true` fails On by default, and listens on `httpsPort` (default 443) on every vhost beside the plain-http `port` (default 80). -The issuer is a **host-held hive CA**, not a bare self-signed leaf. A host service (`hive-tls-ca.service`, from the `hive-tls` module) generates a long-lived CA (`services.hyperhive.tls.caValidityDays`, default ~20y) under `services.hyperhive.tls.stateDir` (default `/var/lib/hive-tls`), then signs a gateway **leaf** (`leafValidityDays`, default 30d) with it. `hive-gateway-self-signed-cert` then imports the leaf into nginx's state dir (`/var/lib/hive-gateway/tls/{cert,key}.pem`). +The issuer is a **host-held hive CA**, not a bare self-signed leaf. A host service (`hive-tls-ca.service`, from the `hive-tls` module) generates a long-lived CA (`services.hyperhive.deploy.hive-controller.tls.caValidityDays`, default ~20y) under `services.hyperhive.deploy.hive-controller.tls.stateDir` (default `/var/lib/hive-tls`), then signs a gateway **leaf** (`leafValidityDays`, default 30d) with it. `hive-gateway-self-signed-cert` then imports the leaf into nginx's state dir (`/var/lib/hive-gateway/tls/{cert,key}.pem`). ⚠️ **Do not collapse that import unit into pointing nginx at the CA dir.** It does two jobs, and skipping it has taken the gateway down in production diff --git a/docs/swarm/ca.md b/docs/swarm/ca.md index 19cd15eb..f9e24793 100644 --- a/docs/swarm/ca.md +++ b/docs/swarm/ca.md @@ -79,7 +79,8 @@ sign leaves, and the chain stops there. `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 +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: @@ -136,7 +137,7 @@ No automatic adoption. `hive-tls-ca.service` fails, loudly, naming both certificates and giving the two-command recipe: ``` -rm /ca.pem /ca-key.pem +rm /ca.pem /ca-key.pem systemctl restart hive-tls-ca.service ``` diff --git a/docs/swarm/secrets.md b/docs/swarm/secrets.md index 5f25a132..d046f5b9 100644 --- a/docs/swarm/secrets.md +++ b/docs/swarm/secrets.md @@ -123,8 +123,8 @@ when the system builds. The server names the offending file and refuses to run. | secret | generated by | lives at | |---|---|---| -| hive CA cert + key | `hive-tls.nix` first-boot unit | `/ca.pem`, `ca-key.pem` (`0600`) | -| hive leaf certs | `hive-tls.nix`, signed by the hive CA | `/.pem` | +| hive CA cert + key | `hive-tls.nix` first-boot unit | `/ca.pem`, `ca-key.pem` (`0600`) | +| hive leaf certs | `hive-tls.nix`, signed by the hive CA | `/.pem` | | matrix registration token | a host activation script, on first boot | `/var/lib/hyperhive/matrix-register-token` (`0600`) | | the forge's copy of its OIDC secret | `hive-forge-oidc-secret.service` copies it from authelia's tree | `/var/lib/forgejo-oidc/.secret` inside the forge container | | the homeserver's copy of its OIDC secret | `hive-matrix-oidc-secret.service`, same shape | `/var/lib/tuwunel-oidc/.secret`, handed to tuwunel through `LoadCredential` | diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index c2e75b93..d667c8d9 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -819,8 +819,9 @@ const HIVE_CA_FILE: &str = "hive-ca.pem"; /// Host path of the hive's TLS trust anchors, when self-signed TLS is /// active. The hyperhive TLS module sets `HIVE_TLS_CA_PATH` in hive-c0re's env -/// (to `/trust-bundle.pem`) whenever the gateway serves a -/// self-signed, hive-CA-signed leaf. The file holds one *or more* certs — +/// (to `/trust-bundle.pem`) whenever the +/// gateway serves a self-signed, hive-CA-signed leaf. The file holds one +/// *or more* certs — /// the hive CA plus the swarm root it is issued under — and is copied /// verbatim, which `security.pki.certificateFiles` accepts; nothing here /// parses it. Returns `Some(path)` only when the var is set AND diff --git a/hivectl/src/wg.rs b/hivectl/src/wg.rs index 933f47f3..272e1e05 100644 --- a/hivectl/src/wg.rs +++ b/hivectl/src/wg.rs @@ -15,7 +15,8 @@ const WG_KEY_PATH: &str = "/etc/wireguard/hive.key"; const WG_INTERFACE: &str = "wg-hive"; /// Host path of this hive's TLS trust bundle (matches the -/// `services.hyperhive.tls.stateDir` default in hive-tls.nix). Its +/// `services.hyperhive.deploy.hive-controller.tls.stateDir` default in +/// hive-tls.nix). Its /// existence means the gateway serves a self-signed, hive-CA-signed /// leaf, so a federating peer needs an anchor for it — which is now the /// swarm root, not a per-hive CA. Absent = ACME / operator cert, trusted diff --git a/nix/host-modules/deploy.nix b/nix/host-modules/deploy.nix index 16b4ce8f..a2551a1a 100644 --- a/nix/host-modules/deploy.nix +++ b/nix/host-modules/deploy.nix @@ -129,6 +129,25 @@ in [ "services" "hyperhive" "enableAllLocalDefaults" ] [ "services" "hyperhive" "deploy" "singleHostSwarm" ] ) + + # The hive CA's own knobs. They sat at the TOP of `services.hyperhive`, + # which is meant to be everything about hyperhive rather than the settings + # of one hive — and where the CA lives, how long it lasts and how long its + # leaves last are decisions of the host that holds the key. `hive-controller` + # is hive-c0re's new name (mara, on the issue), so the daemon that owns the + # CA is what they hang off. + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "tls" "stateDir" ] + [ "services" "hyperhive" "deploy" "hive-controller" "tls" "stateDir" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "tls" "caValidityDays" ] + [ "services" "hyperhive" "deploy" "hive-controller" "tls" "caValidityDays" ] + ) + (lib.mkRenamedOptionModule + [ "services" "hyperhive" "tls" "leafValidityDays" ] + [ "services" "hyperhive" "deploy" "hive-controller" "tls" "leafValidityDays" ] + ) ]; # ⚠️ `deploy.forgejo` is declared in ./hive-ci.nix, not here, and it is the diff --git a/nix/host-modules/hive-c0re/default.nix b/nix/host-modules/hive-c0re/default.nix index ed3c398d..2d534249 100644 --- a/nix/host-modules/hive-c0re/default.nix +++ b/nix/host-modules/hive-c0re/default.nix @@ -15,7 +15,7 @@ let caTrust = import ../lib/hive-ca-trust.nix { inherit lib; - tlsCfg = config.services.hyperhive.tls; + tlsCfg = config.services.hyperhive.deploy.hive-controller.tls; gatewayCfg = config.services.hyperhive.gateway; }; diff --git a/nix/host-modules/hive-ci.nix b/nix/host-modules/hive-ci.nix index 3725cbfe..ef316a5b 100644 --- a/nix/host-modules/hive-ci.nix +++ b/nix/host-modules/hive-ci.nix @@ -9,7 +9,7 @@ let forgeCfg = config.services.hyperhive.swarm.forge; gatewayCfg = config.services.hyperhive.gateway; networkCfg = config.services.hyperhive.network; - tlsCfg = config.services.hyperhive.tls; + tlsCfg = config.services.hyperhive.deploy.hive-controller.tls; # Self-signed TLS is the gateway default (no operator cert / ACME). When # active, forgejo's ROOT_URL is `https://forge.` and the leaf is diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index 1978346b..941f8d84 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -9,7 +9,7 @@ let gatewayCfg = config.services.hyperhive.gateway; hyperhiveDomain = config.services.hyperhive.domain; swarmDomain = config.services.hyperhive.swarm.domain; - tlsCfg = config.services.hyperhive.tls; + tlsCfg = config.services.hyperhive.deploy.hive-controller.tls; # Forgejo's name for the login source. A constant, not an option: it # is the key this module's own idempotency check looks up, so making diff --git a/nix/host-modules/hive-gateway/default.nix b/nix/host-modules/hive-gateway/default.nix index 2adbbb1e..5703b588 100644 --- a/nix/host-modules/hive-gateway/default.nix +++ b/nix/host-modules/hive-gateway/default.nix @@ -94,7 +94,7 @@ let # from the tls option here rather than importing that helper: this # is the HOST's own bundle, and the helper's job is assembling a # per-container copy. - caBundle = "${config.services.hyperhive.tls.stateDir}/trust-bundle.pem"; + caBundle = "${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/trust-bundle.pem"; }; nginxTree = import ./vhosts.nix { @@ -346,8 +346,8 @@ in # read it — a 0600 root:root key passes the master load but # fails the pre-start config test with `BIO_new_file() … # Permission denied`, blocking the unit. Cert is world-read. - install -m 0644 ${config.services.hyperhive.tls.stateDir}/gateway.pem ${tlsCert} - install -m 0640 -g nginx ${config.services.hyperhive.tls.stateDir}/gateway-key.pem ${tlsKey} + install -m 0644 ${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/gateway.pem ${tlsCert} + install -m 0640 -g nginx ${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/gateway-key.pem ${tlsKey} # The swarm-services leaf, when this host issues one. It is # a separate pair rather than more SANs on the one above @@ -372,13 +372,13 @@ in # # Measured, not theorised: this exact path took pr1ma's # gateway down when the services sub-CA failed to issue. - if [ -s ${config.services.hyperhive.tls.stateDir}/swarm-services.pem ]; then - install -m 0644 ${config.services.hyperhive.tls.stateDir}/swarm-services.pem ${svcCert} - install -m 0640 -g nginx ${config.services.hyperhive.tls.stateDir}/swarm-services-key.pem ${svcKey} + if [ -s ${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/swarm-services.pem ]; then + install -m 0644 ${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/swarm-services.pem ${svcCert} + install -m 0640 -g nginx ${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/swarm-services-key.pem ${svcKey} else echo "no swarm-services leaf — serving the hive leaf on those names (mismatch, not an outage)" >&2 - install -m 0644 ${config.services.hyperhive.tls.stateDir}/gateway.pem ${svcCert} - install -m 0640 -g nginx ${config.services.hyperhive.tls.stateDir}/gateway-key.pem ${svcKey} + install -m 0644 ${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/gateway.pem ${svcCert} + install -m 0640 -g nginx ${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/gateway-key.pem ${svcKey} fi ''; }; diff --git a/nix/host-modules/hive-matrix.nix b/nix/host-modules/hive-matrix.nix index 8d1d33bd..002dcb18 100644 --- a/nix/host-modules/hive-matrix.nix +++ b/nix/host-modules/hive-matrix.nix @@ -7,7 +7,7 @@ let cfg = config.services.hyperhive.swarm.matrix; networkCfg = config.services.hyperhive.network; - tlsCfg = config.services.hyperhive.tls; + tlsCfg = config.services.hyperhive.deploy.hive-controller.tls; gatewayCfg = config.services.hyperhive.gateway; hyperhiveDomain = config.services.hyperhive.domain; diff --git a/nix/host-modules/hive-tls.nix b/nix/host-modules/hive-tls.nix index b8f1010d..db3163b6 100644 --- a/nix/host-modules/hive-tls.nix +++ b/nix/host-modules/hive-tls.nix @@ -5,7 +5,7 @@ ... }: let - cfg = config.services.hyperhive.tls; + cfg = config.services.hyperhive.deploy.hive-controller.tls; hyperhiveCfg = config.services.hyperhive; gatewayCfg = config.services.hyperhive.gateway; swarmCaCfg = config.services.hyperhive.swarm.ca; @@ -287,7 +287,7 @@ in # everything this hive serves. Either way an existing CA is left # alone; see the issuance comment below. - options.services.hyperhive.tls = { + options.services.hyperhive.deploy.hive-controller.tls = { stateDir = lib.mkOption { type = lib.types.str; default = "/var/lib/hive-tls"; diff --git a/nix/host-modules/lib/hive-ca-trust.nix b/nix/host-modules/lib/hive-ca-trust.nix index d6e5e428..37443f5a 100644 --- a/nix/host-modules/lib/hive-ca-trust.nix +++ b/nix/host-modules/lib/hive-ca-trust.nix @@ -21,7 +21,7 @@ # # systemd.services."container@hive-ci" = lib.mkMerge [ caTrust.containerOrdering … ]; # # environment.NODE_EXTRA_CA_CERTS = caTrust.caContainerPath; # consumption, per-caller # -# `tlsCfg` = config.services.hyperhive.tls +# `tlsCfg` = config.services.hyperhive.deploy.hive-controller.tls # `gatewayCfg` = config.services.hyperhive.gateway { lib, diff --git a/nix/host-modules/otel.nix b/nix/host-modules/otel.nix index 2b6c32a1..9bff01ae 100644 --- a/nix/host-modules/otel.nix +++ b/nix/host-modules/otel.nix @@ -34,7 +34,7 @@ let # `opentelemetry-collector` service holding an `SSL_CERT_FILE`. caTrust = import ./lib/hive-ca-trust.nix { inherit lib; - tlsCfg = config.services.hyperhive.tls; + tlsCfg = config.services.hyperhive.deploy.hive-controller.tls; gatewayCfg = config.services.hyperhive.gateway; }; in diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index 54e182b8..cdad7b38 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -75,8 +75,8 @@ let # Where the leaf lands for openbao to read. Inside the container, because # `hostStateDir` is already bind-mounted at `stateDir` — so the delivery - # below needs no second mount, and nothing has to bind `tls.stateDir`, - # which holds the hive CA's private key. + # below needs no second mount, and nothing has to bind + # `deploy.hive-controller.tls.stateDir`, which holds the hive CA's private key. serverCertPath = "${stateDir}/server.pem"; serverKeyPath = "${stateDir}/server-key.pem"; diff --git a/nix/host-modules/swarm-ca.nix b/nix/host-modules/swarm-ca.nix index f4d5c8cd..29a29bfe 100644 --- a/nix/host-modules/swarm-ca.nix +++ b/nix/host-modules/swarm-ca.nix @@ -81,10 +81,10 @@ in With it off, both artifacts are operator-provided: the root under `stateDir`, and this hive's CA under - `services.hyperhive.tls.stateDir`. A hive given neither keeps - the self-signed CA it has always had — it simply isn't part of - a swarm's trust hierarchy, which is the correct outcome for a - hive nobody has federated yet. + `services.hyperhive.deploy.hive-controller.tls.stateDir`. A hive + given neither keeps the self-signed CA it has always had — it + simply isn't part of a swarm's trust hierarchy, which is the + correct outcome for a hive nobody has federated yet. ''; }; @@ -126,7 +126,7 @@ in default = 10950; description = '' Validity window of the swarm root CA in days (default ~30y). - Deliberately longer than `services.hyperhive.tls.caValidityDays`: + Deliberately longer than `services.hyperhive.deploy.hive-controller.tls.caValidityDays`: the root must outlive the hive CAs it issues, or those chains expire out from under hives that are still perfectly happy with their own intermediate. Rotating a root is the one operation in diff --git a/nix/host-modules/swarm-controller.nix b/nix/host-modules/swarm-controller.nix index b54686ba..01c8e316 100644 --- a/nix/host-modules/swarm-controller.nix +++ b/nix/host-modules/swarm-controller.nix @@ -52,7 +52,7 @@ let # phantom `swarm-controller` service holding an `SSL_CERT_FILE`. caTrust = import ./lib/hive-ca-trust.nix { inherit lib; - tlsCfg = config.services.hyperhive.tls; + tlsCfg = config.services.hyperhive.deploy.hive-controller.tls; gatewayCfg = config.services.hyperhive.gateway; }; diff --git a/nix/host-modules/swarm-nats.nix b/nix/host-modules/swarm-nats.nix index e5266cb4..d4c68b12 100644 --- a/nix/host-modules/swarm-nats.nix +++ b/nix/host-modules/swarm-nats.nix @@ -25,7 +25,7 @@ let machine = "swarm-nats"; - tlsCfg = config.services.hyperhive.tls; + tlsCfg = config.services.hyperhive.deploy.hive-controller.tls; gatewayCfg = config.services.hyperhive.gateway; caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; }; # The responder introspects authelia over https BY NAME. Its HTTP client is