feat(nix): issue a swarm-services leaf, and renew it with the hive one

The swarm's service names cannot go on the hive leaf: the hive CA is
name-constrained to the hive domain and those names are siblings of
it. So there is a second leaf, signed by the services sub-CA.

signLeafScript is parameterised rather than duplicated -- same
ceremony, different issuer and names -- so the two cannot drift in
how they are built. The name list itself is derived once, as a
read-only swarm.serviceDomains, and read by both the sub-CA that
name-constrains those names and the leaf that carries them as SANs:
two modules each assembling the list is how they stop agreeing.

The renewal unit is the point of this commit as much as the leaf.
hive-tls-resign now knows about both, because a leaf that first-boot
issuance creates and weekly renewal ignores looks perfect for its
entire validity and then expires with no warning -- the failure is
invisible until it is total. The freshness test became a function
over a leaf rather than a check of one, so adding a third leaf is a
line rather than a rewrite.

The services leaf is skipped where the sub-CA is absent: it exists
only where the swarm CA is autoconfigured, and on a hive whose certs
come from its operator the correct state is no leaf, not a stale one.

Also drops a comment that documented signLeafScript's old signature
from above an unrelated binding.
This commit is contained in:
atlas 2026-08-05 21:16:25 +02:00 committed by mara
commit 5a83c40dca
3 changed files with 114 additions and 45 deletions

View file

@ -51,23 +51,12 @@ let
else
"hyperhive";
# The names the services sub-CA is allowed to issue for, read from the
# service options rather than spelled out here: a constraint list that
# doesn't track its own consumers is a constraint that silently stops
# covering one.
#
# Sorted + deduplicated because this list is also the *rotation
# trigger* below — an unstable order would re-issue the CA on every
# rebuild, and a CA that churns is one nothing can pin.
serviceDomains = lib.sort (a: b: a < b) (
lib.unique (
lib.filter (d: d != null && d != "") [
hyperhiveCfg.swarm.forge.domain
hyperhiveCfg.swarm.matrix.gatewayHost
hyperhiveCfg.swarm.authelia.domain
]
)
);
# Derived once in ./swarm.nix, read here and by ./hive-tls.nix: the
# CA that name-constrains these and the leaf that carries them as SANs
# must agree exactly, and two modules each assembling the list is how
# they stop agreeing. It is also this unit's *rotation trigger* below,
# which is why the ordering is stable there rather than here.
serviceDomains = hyperhiveCfg.swarm.serviceDomains;
in
{
options.services.hyperhive.swarm.ca = {