diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index 5c5194c1..da924288 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,29 @@ 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 EITHER the cert or key is + # missing/empty, OR the cert fails an openssl parse — + # catches truncated / corrupt leftovers from a previous + # interrupted run AND the "cert clean but key absent" + # edge case (argus 🟡 on the first revision) which + # otherwise tripped `chmod 0600 ${tlsKey}` below with + # ENOENT under `set -eu`. The whole oneshot is safe to + # re-run; a healthy cert+key pair is left alone. + if [ ! -s ${tlsCert} ] || [ ! -s ${tlsKey} ] || ! 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} '';