diff --git a/nix/modules/hive-tls.nix b/nix/modules/hive-tls.nix index 0931e035..2e6eaaba 100644 --- a/nix/modules/hive-tls.nix +++ b/nix/modules/hive-tls.nix @@ -73,13 +73,10 @@ in when it is missing or near expiry; because it shares the CA anchor, a rotation does not disturb consumer trust. Agents and federation peers validate against the CA, not browser CA/B-forum - limits. NOTE: at this short a window the re-sign must run more - often than the leaf lifetime. Today `hive-tls-ca` re-signs at - service activation (boot/rebuild) only — there is no periodic - re-sign timer yet — so a hive that does not reboot within the - validity window would let the leaf expire. Keep that in mind (or - add a re-sign timer) when running a hive that stays up for long - stretches without a rebuild. + limits. The weekly `hive-tls-resign` timer re-signs the leaf once + it is within half its validity of expiry and propagates the new + leaf into the running gateway, so a long-uptime host renews + automatically without a reboot. ''; }; }; @@ -165,6 +162,104 @@ in ''; }; + # Weekly re-sign of the gateway leaf so short-lived leaves renew + # without depending on a reboot. + # + # `hive-tls-ca` only re-signs at service activation (boot/rebuild); a + # long-uptime host would otherwise let a 30-day leaf lapse silently. + # This service re-signs the leaf directly (not by bouncing hive-tls-ca) + # and propagates the new leaf into the running gateway container when + # the file actually changed. + # + # Propagation mechanism: nginx in the gateway container serves a *copy* + # of the leaf written by `hive-gateway-self-signed-cert` (which runs at + # container start). A host-side `systemctl -M hive-gateway` call + # triggers the re-import + reload, mirroring how hive-c0re reloads the + # gateway after each agents.conf write. A path unit *inside* the + # container was tried first but does not work: IN_MOVED_TO from an + # atomic rename on the host does not propagate across the nspawn + # mount-namespace boundary. + # + # `|| true` on propagation so a stopped gateway never fails the unit — + # its next boot will import the already-rotated leaf anyway. + systemd.services.hive-tls-resign = { + description = "Re-sign the gateway TLS leaf and propagate it into the gateway container"; + path = [ + pkgs.openssl + pkgs.coreutils + pkgs.systemd + ]; + serviceConfig = { + Type = "oneshot"; + UMask = "0077"; + SyslogIdentifier = "hive-tls-resign"; + }; + script = '' + set -euo pipefail + d=${lib.escapeShellArg cfg.stateDir} + ca="$d/ca.pem" + cak="$d/ca-key.pem" + leaf="$d/gateway.pem" + leafk="$d/gateway-key.pem" + + # Re-sign only when the leaf is within half its validity of expiry. + # The weekly cadence catches this window well before the leaf lapses. + halflife=$(( ${toString cfg.leafValidityDays} * 86400 / 2 )) + if [ -s "$leaf" ] && \ + openssl x509 -in "$leaf" -noout -checkend "$halflife" >/dev/null 2>&1; then + echo "gateway leaf valid for more than half its lifetime — no resign needed" + exit 0 + fi + + echo "gateway leaf missing or near expiry — re-signing under current CA" + before="$(sha256sum "$leaf" 2>/dev/null || true)" + + csr="$(mktemp "$d/gateway.csr.XXXXXX")" + ext="$(mktemp "$d/leaf.ext.XXXXXX")" + trap 'rm -f "$csr" "$ext"' EXIT + + openssl req -newkey rsa:4096 -nodes -sha256 \ + -keyout "$leafk" -out "$csr" \ + -subj "/CN=${domain}" + + { + printf 'subjectAltName=DNS:%s,DNS:forge.%s,DNS:matrix.%s,DNS:*.%s\n' \ + ${lib.escapeShellArg domain} ${lib.escapeShellArg domain} \ + ${lib.escapeShellArg domain} ${lib.escapeShellArg domain} + printf 'basicConstraints=critical,CA:FALSE\n' + printf 'keyUsage=critical,digitalSignature,keyEncipherment\n' + printf 'extendedKeyUsage=serverAuth\n' + } > "$ext" + + openssl x509 -req -in "$csr" -CA "$ca" -CAkey "$cak" \ + -CAcreateserial -days ${toString cfg.leafValidityDays} -sha256 \ + -extfile "$ext" -out "$leaf" + chmod 0600 "$leafk" + chmod 0644 "$leaf" + + after="$(sha256sum "$leaf" 2>/dev/null || true)" + if [ "$before" != "$after" ]; then + echo "gateway leaf rotated — propagating into hive-gateway" + systemctl -M hive-gateway restart hive-gateway-self-signed-cert.service || true + systemctl -M hive-gateway reload nginx.service || true + else + echo "gateway leaf unchanged (already up to date)" + fi + ''; + }; + + systemd.timers.hive-tls-resign = { + description = "Weekly gateway-leaf re-sign and propagation"; + wantedBy = [ "timers.target" ]; + timerConfig = { + # Run weekly; Persistent=true fires a missed run on next boot if + # the timer was not active (e.g. the host was off on the scheduled + # day), preventing a dormant timer from letting the leaf lapse. + OnCalendar = "weekly"; + Persistent = true; + }; + }; + # Signal the hive-c0re lifecycle that a hive CA exists: it bind-mounts # this file (read-only, the CA cert ONLY — never the key) into each # agent container so agents + their tools can trust the gateway's