From 25d5b4b69c5f1d3e6e825726bab7c09a8b494bb5 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 12 Aug 2026 16:41:59 +0200 Subject: [PATCH 1/2] fix(3149): the forge container can resolve the authelia name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The login source still failed after the argv fix, with dial tcp: lookup auth.constellation.darkest.space: no such host The hive's dnsmasq is authoritative for the swarm service names, but only containers whose resolv.conf points at the bridge ask it — agent containers do, via an explicit unit written for that reason (nix/agent-modules/network.nix). hive-forge resolves through the host's resolvers instead, and the swarm domain has no public records, so discovery fails for a name that resolves fine one container over. Publish it in the container's own hosts file, mapped to 127.0.0.1: sharing the host netns, loopback is the host, where nginx already serves that vhost. TLS still validates - the CA trust bundle is bind-mounted and the leaf covers the name. Gated on authelia being local, since a remote provider's name belongs to another machine. Gate (state/eval-3149-hosts.sh) asserts the RENDERED /etc/hosts rather than the option, plus an absence probe with SSO off and a check that the discovery URL names the same host the entry publishes. --- nix/host-modules/hive-forge/default.nix | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/nix/host-modules/hive-forge/default.nix b/nix/host-modules/hive-forge/default.nix index 0967fafe..2cc01ce6 100644 --- a/nix/host-modules/hive-forge/default.nix +++ b/nix/host-modules/hive-forge/default.nix @@ -535,6 +535,28 @@ in # all filtering; never run one in here. networking.firewall.enable = false; + # Teach this container the SSO name, because nothing else will. + # + # The hive's dnsmasq is authoritative for the swarm service + # names, but only containers whose resolv.conf points at the + # bridge ask it — agent containers do, by an explicit unit + # (`nix/agent-modules/network.nix`) written for exactly this + # reason. This container resolves through the host's resolvers + # instead, and the swarm domain has no public records, so + # `admin auth add-oauth` fails at discovery with "no such + # host" while the same name resolves fine one container over. + # + # `127.0.0.1` rather than the bridge IP: sharing the host netns + # means loopback IS the host, where nginx serves this vhost. + # TLS still validates — the CA trust bundle is bind-mounted + # above, and the leaf covers this name. + # + # Only when THIS host runs authelia. With a remote provider the + # name belongs to another machine and must resolve normally. + networking.hosts = lib.mkIf ssoLocal { + "127.0.0.1" = [ autheliaCfg.domain ]; + }; + services.forgejo = { enable = true; package = cfg.package; From 0b1b08dfe6934c2f9212a724c713e16e277f5ec4 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 12 Aug 2026 17:34:57 +0200 Subject: [PATCH 2/2] fix(3149): the host asks the hive's resolver, at the bridge IP Per mara: a general fix, not one name in one container. Every container inherits a COPY of the host's /etc/resolv.conf at start (nixos-containers.nix: cp --remove-destination, one shot, not a bind-mount), so the address written there is the address every container tries - in its own netns. That makes the value load-bearing: value host host-netns containers bridged containers 127.0.0.1 ok ok THEIR OWN loopback bridge IP ok ok ok dnsmasq binds lo and the bridge, so the bridge IP works for the host too. It is the only value correct on both sides of a netns boundary. resolveLocalQueries goes ON for its plumbing, not its address: it points dnsmasq's own upstreams at a separate resolv-file, without which dnsmasq reads /etc/resolv.conf and every non-hive query loops the moment the host is pointed at dnsmasq. Its two loopback-publishing effects (networking.nameservers and resolvconf.useLocalResolver) are overridden. Cost: the host's DNS now depends on dnsmasq being up. Every container already did. The forge container keeps its hosts entry from the previous commit - not redundancy, a fallback in a different failure domain: it works with no DNS at all, so SSO does not ride on a host-wide resolver change. --- nix/host-modules/hive-gateway/default.nix | 26 +++++++++++++++++++++++ nix/host-modules/hive-gateway/dnsmasq.nix | 17 +++++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/nix/host-modules/hive-gateway/default.nix b/nix/host-modules/hive-gateway/default.nix index b573a93a..9f3b4306 100644 --- a/nix/host-modules/hive-gateway/default.nix +++ b/nix/host-modules/hive-gateway/default.nix @@ -157,6 +157,32 @@ in "f /var/lib/hive-gateway/conf/gateway.htpasswd 0644 hive-core hive-core - -" ]; + # The host asks the hive's own resolver, at the BRIDGE IP. + # + # Every container inherits a COPY of this host's `/etc/resolv.conf` + # at start (`nixos-containers.nix`: `cp --remove-destination`, one + # shot, not a bind-mount) — so whatever address is written here is + # the address every container will try, in its own netns. + # + # 🚨 That is why this is the bridge IP and not `127.0.0.1`, and the + # distinction is load-bearing rather than stylistic: + # + # value host host-netns containers bridged containers + # 127.0.0.1 ok ok THEIR OWN loopback + # bridge IP ok ok ok + # + # dnsmasq binds both `lo` and the bridge (./dnsmasq.nix), so the + # bridge IP is reachable from the host too — it is the only value + # correct on both sides of a netns boundary. `resolveLocalQueries` + # publishes loopback by default, hence both overrides here; the + # flag stays on for its `resolv-file` plumbing, which is what keeps + # dnsmasq's own upstreams out of the file we are pointing at it. + # + # Cost, stated because it is real: the host's DNS now depends on + # dnsmasq being up. Every container already did. + networking.nameservers = lib.mkForce [ networkCfg.bridgeIp ]; + networking.resolvconf.useLocalResolver = lib.mkForce false; + # ACME (Let's Encrypt) integration. nginx vhosts set # `enableACME = true` via the vhost builder; this provides the # shared ACME config (acceptTerms + email). diff --git a/nix/host-modules/hive-gateway/dnsmasq.nix b/nix/host-modules/hive-gateway/dnsmasq.nix index cd56e226..e790cf27 100644 --- a/nix/host-modules/hive-gateway/dnsmasq.nix +++ b/nix/host-modules/hive-gateway/dnsmasq.nix @@ -16,10 +16,19 @@ }: { enable = true; - # Don't substitute the container's /etc/resolv.conf — the gateway - # uses the host's resolver for its own outbound traffic; dnsmasq is - # purely for incoming queries from agent containers. - resolveLocalQueries = false; + # ON for its *plumbing*, not for the address it publishes. + # + # This flag does two separable things upstream. The one that matters + # here: it points dnsmasq's own upstream servers at a SEPARATE file + # (`resolv-file = /etc/dnsmasq-resolv.conf`, kept current by + # resolvconf). Without that, dnsmasq reads `/etc/resolv.conf` for its + # upstreams — so the moment the host's resolver is pointed at dnsmasq, + # every non-hive query goes in a circle. + # + # The other thing it does is publish `127.0.0.1` as the host's + # nameserver, which is the wrong address for this hive: see the + # `nameservers` override in ./default.nix, where the reason lives. + resolveLocalQueries = true; settings = { # Bind only on the bridge interface (and lo for health-checks). # Outside hosts can't even see the listener.