Compare commits

...
Author SHA1 Message Date
atlas
0b1b08dfe6 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.
2026-08-12 21:09:21 +02:00
atlas
25d5b4b69c fix(3149): the forge container can resolve the authelia name
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.
2026-08-12 21:09:21 +02:00
3 changed files with 61 additions and 4 deletions

View file

@ -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;

View file

@ -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).

View file

@ -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.