diff --git a/nix/host-modules/hive-tls.nix b/nix/host-modules/hive-tls.nix index 1e2087e0..6179f823 100644 --- a/nix/host-modules/hive-tls.nix +++ b/nix/host-modules/hive-tls.nix @@ -514,6 +514,39 @@ in [ -s "$1" ] && openssl x509 -in "$1" -noout -checkend "$halflife" >/dev/null 2>&1 } + # Expiry is not the only way a leaf goes wrong. A leaf signed + # when the configured name set was smaller stays valid for its + # whole lifetime while omitting every name added since — so a + # config change can evaluate, build and deploy cleanly and the + # gateway still serves a certificate that does not cover the new + # service. The services sub-CA already reconciles this way (its + # `.names` comparison in ../swarm-ca.nix); the leaves did not, so + # adding a name to the config alone could not fix a mis-served + # certificate on any hive whose leaf was not already near expiry. + # + # Read the names out of the CERTIFICATE, not a sidecar file: the + # pem is what nginx actually serves, and a bookkeeping file + # drifts from it the moment anyone replaces a leaf by hand. + covers() { # $1 = leaf, $2 = space-separated names it must carry + [ -s "$1" ] || return 1 + _have="$(openssl x509 -in "$1" -noout -ext subjectAltName 2>/dev/null \ + | tr ',' '\n' | sed -n 's/^[[:space:]]*DNS:\(.*\)$/\1/p')" + [ -n "$_have" ] || return 1 + _missing=0 + # ⚠️ The hive leaf carries `*.`; without this the shell + # expands it against the cwd and the comparison silently tests + # a filename. + set -f + for _n in $2; do + printf '%s\n' "$_have" | grep -qxF "$_n" || _missing=1 + done + set +f + [ "$_missing" = 0 ] + } + + hiveNames=${lib.escapeShellArg "${domain} *.${domain}"} + svcNames=${lib.escapeShellArg (lib.concatStringsSep " " swarmServiceDomains)} + # The services leaf is only expected where the sub-CA exists; # elsewhere its absence is the correct state, not a stale leaf. want_svc=${if swarmServiceDomains == [ ] then "0" else "1"} @@ -521,12 +554,13 @@ in want_svc=0 fi - if fresh "$leaf" && { [ "$want_svc" = 0 ] || fresh "$svcleaf"; }; then - echo "leaves valid for more than half their lifetime — no resign needed" + if fresh "$leaf" && covers "$leaf" "$hiveNames" \ + && { [ "$want_svc" = 0 ] || { fresh "$svcleaf" && covers "$svcleaf" "$svcNames"; }; }; then + echo "leaves valid, and covering the configured names — no resign needed" exit 0 fi - echo "a leaf is missing or near expiry — re-signing under the current CAs" + echo "a leaf is missing, near expiry, or missing a configured name — re-signing" before="$(sha256sum "$leaf" "$svcleaf" 2>/dev/null || true)" ${signHiveLeaf}