From 155df39ee44ccf3020a02f5e9c46436ccc43ec43 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 11 Aug 2026 18:20:08 +0200 Subject: [PATCH 1/2] feat(nix): warn when a swarm service name has no certificate path here MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The swarm's service names default to siblings of the hive domain (forge., not forge.), and the hive CA's leaf is a single-label wildcard over its own domain, so it cannot cover them. The swarm-services leaf can — but only on a host that holds the swarm root key, i.e. swarm.ca.autoConfigure. Everywhere else the gateway quietly serves the hive leaf on those names and every client sees a mismatch, on a config that evaluates and deploys cleanly. A warning rather than an assertion, per the operator's call. This module knows what it can issue; it cannot see an operator-installed services sub-CA or an external ACME setup, so "no certificate path" is a thing it observes, not a thing it can conclude. A rebuild must not be blocked by a verdict this host isn't in a position to reach — the message says what was observed and names both ways out. --- nix/host-modules/hive-tls.nix | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/nix/host-modules/hive-tls.nix b/nix/host-modules/hive-tls.nix index 6da7b095..1df51f5f 100644 --- a/nix/host-modules/hive-tls.nix +++ b/nix/host-modules/hive-tls.nix @@ -15,6 +15,21 @@ let # constrained to cannot disagree. swarmServiceDomains = hyperhiveCfg.swarm.serviceDomains; + # True for the names `signHiveLeaf` below actually covers: the hive + # domain itself, or ONE label under it. `*.` is a single-label + # wildcard — `a.b.` does not match it — so the depth check is + # the whole point rather than a nicety. + coveredByHiveLeaf = + n: + n == domain + || (lib.hasSuffix ".${domain}" n && !lib.hasInfix "." (lib.removeSuffix ".${domain}" n)); + + # Service names this host can serve a *matching* certificate for, and + # the ones it can't. A name is issuable here when the hive leaf covers + # it, or when this host issues the swarm-services leaf — which needs + # the swarm root's private key, i.e. `swarm.ca.autoConfigure`. + uncoveredServiceDomains = lib.filter (n: !coveredByHiveLeaf n) swarmServiceDomains; + # The host-managed hive CA is the trust anchor for self-signed mode. # It is only stood up when the gateway actually serves a self-signed # cert: the gateway must be in self-signed mode. `domain` is required @@ -244,6 +259,41 @@ in }; config = lib.mkIf active { + # A swarm service name that is a sibling of the hive domain rather + # than a child needs the swarm-services leaf, and this host only + # signs that one when it holds the swarm root key. Without it nginx + # falls back to the hive leaf on those names and every client sees a + # name mismatch — on a config that evaluates and deploys cleanly. + # + # ⚠️ A WARNING, NOT AN ASSERTION, and the distinction is the point: + # this module can see what *it* can issue; it cannot see an + # operator-installed services sub-CA, an external ACME setup, or a + # cert delivered by any other means. A rebuild must not be blocked + # by a conclusion this host is not in a position to reach. Report + # the observation and let the operator judge it. + # + # (`certDir` / ACME modes don't reach here at all: `active` is + # self-signed-only, and there the operator's cert decides.) + warnings = lib.optional (uncoveredServiceDomains != [ ] && !swarmCaCfg.autoConfigure) '' + services.hyperhive: these swarm service names are outside this + hive's domain (${domain}), so the hive CA's leaf does not cover + them: + + ${lib.concatMapStringsSep "\n" (n: " ${n}") uncoveredServiceDomains} + + services.hyperhive.swarm.ca.autoConfigure is false, so this host + does not sign the swarm-services leaf either, and the gateway will + serve the hive leaf on those names — a certificate-name mismatch + for browsers and for agents' git-over-https. + + If you have already arranged certificates for them — an + operator-installed services sub-CA in ${swarmCaCfg.stateDir}, or + services.hyperhive.gateway.tls.{certDir,acme} — this is expected + and you can ignore it. Otherwise pin the names back under + ${domain} (services.hyperhive.swarm.{forge.domain, + matrix.gatewayHost, authelia.domain}) or install the sub-CA. + ''; + # Generate (and rotate) the hive CA + gateway leaf before the gateway # container starts. Idempotent: the CA is created once and reused; the # leaf is re-signed on expiry under the same CA so the anchor is stable. From 44164d1a891e01ed527ed02bb6cd4789bedf6036 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 11 Aug 2026 18:26:12 +0200 Subject: [PATCH 2/2] docs(swarm): what the uncovered-service-name warning means MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Names the two ways out (install the sub-CA, or pin back under the hive domain) and says why it is a warning rather than an assertion — the module can see what it issues, not what the operator installed. --- docs/swarm/ca.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/swarm/ca.md b/docs/swarm/ca.md index 5f6ce360..537087f9 100644 --- a/docs/swarm/ca.md +++ b/docs/swarm/ca.md @@ -37,6 +37,27 @@ Moving the swarm CA onto its own host is then a matter of moving `services.hyperhive.swarm.ca.stateDir` and leaving `autoConfigure` off — there is no second code path to switch to. +### The warning about uncovered service names + +The swarm's service names (`swarm.forge.domain`, +`swarm.matrix.gatewayHost`, `swarm.authelia.domain`) default to +siblings of the hive domain — `forge.`, not +`forge.`. The hive CA's leaf is a **single-label** wildcard +over the hive's own domain, so it cannot cover them; only the +swarm-services leaf can, and this host signs that one only under +`autoConfigure`. A hive with neither serves the hive leaf on those names +and every client sees a name mismatch. + +So `hive-tls` emits an eval-time **warning** naming the uncovered names. +It is deliberately not an assertion: this module can see what *it* is +able to issue, but not an operator-installed sub-CA in +`swarm.ca.stateDir`, an external ACME setup, or a certificate delivered +by any other means. If you have arranged one, the warning is expected +and can be ignored. Otherwise either install the sub-CA, or pin the +names back under the hive domain — a supported migration, since the +sub-CA is constrained to the *configured* names and the swarm root +carries no name constraints at all. + ## Constraints on the material The root's private key never reaches the nix store: the store is