diff --git a/nix/host-modules/swarm.nix b/nix/host-modules/swarm.nix index 2afbeb2d..8798f2d3 100644 --- a/nix/host-modules/swarm.nix +++ b/nix/host-modules/swarm.nix @@ -24,23 +24,21 @@ let cfg = config.services.hyperhive; swarmCfg = cfg.swarm; - # The service hostnames with the option that set each one, so an - # assertion can name the thing to edit rather than the value to hunt - # for. `serviceDomains` below is the same set flattened; this keeps - # the provenance that flattening drops. - namedServiceDomains = [ - { - option = "services.hyperhive.swarm.forge.domain"; - value = swarmCfg.forge.domain; - } - { - option = "services.hyperhive.swarm.matrix.gatewayHost"; - value = swarmCfg.matrix.gatewayHost; - } - { - option = "services.hyperhive.swarm.authelia.domain"; - value = swarmCfg.authelia.domain; - } + # Public hostnames of the swarm's own services, in declaration order. + # `serviceDomains` below is this set sorted + deduplicated. + # + # ⚠️ These are NOT required to be under `swarm.domain`. An earlier + # revision asserted that, reasoning that the services sub-CA is + # constrained to the swarm's tree — but the sub-CA is constrained to + # the **configured names** (./swarm-ca.nix) and the swarm root carries + # no name constraints at all, so any configured name is issuable. The + # assertion encoded an intended shape, not a property of the code, and + # it rejected the supported migration path: a hive pinning its old + # `forge.` while joining a swarm at a different apex. + serviceDomains' = [ + swarmCfg.forge.domain + swarmCfg.matrix.gatewayHost + swarmCfg.authelia.domain ]; in { @@ -194,48 +192,10 @@ in services.hyperhive.swarm.peerHives = lib.filterAttrs (name: _: name != cfg.hiveName) swarmCfg.hives; services.hyperhive.swarm.serviceDomains = lib.sort (a: b: a < b) ( - lib.unique (lib.filter (d: d != null && d != "") (lib.map (s: s.value) namedServiceDomains)) + lib.unique (lib.filter (d: d != null && d != "") serviceDomains') ); assertions = [ - { - # Every swarm-service name must live under `swarm.domain`. - # - # This is not style. Those names are certified by the - # swarm-services sub-CA (./swarm-ca.nix), which is - # name-constrained; a name outside the swarm's tree has no - # issuer in this deployment at all. Asserting it here turns - # "your certs mysteriously don't work" into an eval failure that - # names the option — and it applies to every deployment, not - # just the all-local one, because the operator bringing their - # own certs needs to know which names they are bringing them - # for. - # - # Guarded on a null `swarm.domain` so the required-domain - # assertion in ./hive-network.nix stays the one that fires for - # that case; two assertions naming the same missing value is - # noise. - assertion = - swarmCfg.domain == null - || lib.all (s: s.value == null || lib.hasSuffix ".${swarmCfg.domain}" s.value) namedServiceDomains; - message = - let - bad = lib.filter ( - s: s.value != null && !lib.hasSuffix ".${swarmCfg.domain}" s.value - ) namedServiceDomains; - in - '' - Swarm service hostnames must be sub-domains of - services.hyperhive.swarm.domain ("${toString swarmCfg.domain}"): - - ${lib.concatMapStringsSep "\n" (s: " ${s.option} = \"${s.value}\";") bad} - - These names are issued by the swarm-services sub-CA, which is - constrained to the swarm's own tree — a name outside it has no - issuer here, and no certificate this deployment can produce - will match it. - ''; - } { # Guarded on `hiveName != null` so the required-hiveName # assertion in ./hyperhive.nix is what fires for that case —