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