fix(#3462): re-sign a leaf when it stops covering the configured names

Expiry was the only re-sign trigger, so a leaf signed when the name set
was smaller stayed valid -- and wrong -- for its whole lifetime. Adding a
service to swarm.serviceDomains reissues the sub-CA (its own .names
reconciliation) but nothing regenerated the leaf nginx actually serves,
which left the previous commit's config change unable to fix anything on
a hive whose gateway leaf was not near expiry.

covers() reads the DNS names back out of the certificate rather than a
sidecar file: the pem is what nginx serves, and a bookkeeping file drifts
from it the moment a leaf is replaced by hand. Applied to both leaves --
the hive leaf has the same defect if the hive domain ever changes.
This commit is contained in:
atlas 2026-08-18 21:43:07 +02:00 committed by mara
commit 2d2f16406f

View file

@ -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 `*.<domain>`; 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}