From 0aa0e5b95a2bdb4e78764754beb876ce93c23146 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 31 May 2026 17:51:59 +0200 Subject: [PATCH] hive-gateway: idempotent cert oneshot, always-run + parse check (#856 follow-up) --- nix/modules/hive-gateway.nix | 44 ++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index 5c5194c1..07cbd697 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -403,21 +403,22 @@ in { system.stateVersion = "26.05"; - # Generate a self-signed cert on first boot if missing. nginx - # `Requires=` this via `requiredBy`, so systemd refuses to - # start nginx until the cert exists — `before=` alone wasn't - # enough (it only orders within a single transaction, but - # nginx was being pulled into a different transaction by - # multi-user.target and started without waiting, #856). Cert - # covers the bare hive domain plus `*.${hyperhiveDomain}` so - # the matrix + forge sub-domains are valid under the same + # Ensure a valid self-signed cert exists before nginx starts. + # nginx `Requires=` this via `requiredBy`, so systemd refuses + # to start nginx until the script succeeds. ALWAYS runs (no + # ConditionPathExists) and is idempotent — that's necessary + # to reconcile broken state left over from prior failed + # boots (a 0700 dir from a stale UMask, a truncated cert + # from an interrupted oneshot, etc.) which a guarded-on- + # missing-cert script would silently skip and leave broken. + # Cert covers the bare hive domain plus `*.${hyperhiveDomain}` + # so the matrix + forge sub-domains are valid under the same # cert. See `docs/gateway.md` ("Self-signed TLS"). systemd.services.hive-gateway-self-signed-cert = lib.mkIf cfg.selfSignedTls { - description = "Generate self-signed TLS cert for hive-gateway"; + description = "Ensure self-signed TLS cert for hive-gateway"; wantedBy = [ "multi-user.target" ]; before = [ "nginx.service" ]; requiredBy = [ "nginx.service" ]; - unitConfig.ConditionPathExists = "!${tlsCert}"; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; @@ -438,16 +439,25 @@ in ); in '' + set -eu mkdir -p ${tlsDir} # 0755 dir so nginx (master starts as root but workers - # drop to the nginx user) can read the cert path - # without traversal failures. Key stays 0600 below. + # drop to the nginx user) can traverse to read the cert + # path. Re-applied every boot in case a prior run left + # a tighter mode behind. Key stays 0600 below. chmod 0755 ${tlsDir} - openssl req -x509 -newkey rsa:4096 -nodes -sha256 -days 3650 \ - -keyout ${tlsKey} \ - -out ${tlsCert} \ - -subj "/CN=${subjectCN}" \ - -addext "subjectAltName=${sanLines}" + # Generate the cert when it's missing OR fails an + # openssl parse — catches truncated / corrupt leftovers + # from a previous interrupted run. The whole oneshot is + # safe to re-run; a healthy cert is left alone. + if [ ! -s ${tlsCert} ] || ! openssl x509 -in ${tlsCert} -noout >/dev/null 2>&1; then + echo "generating fresh self-signed cert at ${tlsCert}" + openssl req -x509 -newkey rsa:4096 -nodes -sha256 -days 3650 \ + -keyout ${tlsKey} \ + -out ${tlsCert} \ + -subj "/CN=${subjectCN}" \ + -addext "subjectAltName=${sanLines}" + fi chmod 0600 ${tlsKey} chmod 0644 ${tlsCert} '';