From c97120f016f0925dbab1385daf394b1ea25e4c3d Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 3 Jun 2026 16:01:23 +0200 Subject: [PATCH 1/3] fix: forge URL + firewall for isolateContainers=true When containers run in private netns (isolateContainers=true), host loopback is unreachable so HIVE_FORGE_URL=http://127.0.0.1:3000 breaks. - nix/modules/hive-network.nix: when isolateContainers is on + forge is enabled, open forge.httpPort on the bridge interface so agents can reach forgejo at bridgeIp:httpPort (forgejo binds 0.0.0.0) - nix/modules/hive-c0re.nix: HIVE_FORGE_URL switches to bridge IP when network.enable && isolateContainers; loopback path retained when isolateContainers=false - docs/network.md: add Forge access + Forge URL rows to effects table - docs/gateway.md: rewrite HIVE_FORGE_URL section for both modes --- docs/gateway.md | 17 ++++++++++++----- docs/network.md | 2 ++ nix/modules/hive-c0re.nix | 19 +++++++++++++------ nix/modules/hive-network.nix | 11 +++++++++++ 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/docs/gateway.md b/docs/gateway.md index 1c59997a..ba28f942 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -200,13 +200,20 @@ dashboard reach by design — the surface is privileged (approve / deny / destroy) and must not be exposed without a real reverse proxy in front. -## `HIVE_FORGE_URL`: loopback for in-cluster, sub-domain for the operator +## `HIVE_FORGE_URL`: bridge gateway for isolated agents, loopback for shared-netns Agents poll `HIVE_FORGE_URL` for Forgejo notifications + run all -`hive-forge` calls against it. `hive-c0re.nix` pins this to -`http://127.0.0.1:` for the in-cluster path: every -agent container shares the host's network namespace, so loopback -reaches the forge container directly with no DNS lookup needed. +`hive-forge` calls against it. `hive-c0re.nix` sets this based on the +network isolation mode: + +- **`network.isolateContainers = true`**: agents run in private netns, + so host loopback is unreachable. `HIVE_FORGE_URL` is set to + `http://:`. Forgejo binds `0.0.0.0` so it's + reachable at the bridge gateway IP. `hive-network.nix` opens + `forge.httpPort` on the bridge interface automatically. +- **`network.isolateContainers = false`** (default): agents share the host's + network namespace, so loopback reaches forgejo directly. `HIVE_FORGE_URL` + is `http://127.0.0.1:`. The sub-domain default (`forge.`) is for **operator browsers + cross-host clients**, not in-cluster traffic. Using the diff --git a/docs/network.md b/docs/network.md index 1b74a7e3..b5f928d7 100644 --- a/docs/network.md +++ b/docs/network.md @@ -108,6 +108,8 @@ agent containers from shared host netns to private netns. Set only after | IP forwarding | `boot.kernel.sysctl."net.ipv4.ip_forward" = 1` | | Internet NAT | `networking.nat { enable = true; internalInterfaces = [ bridgeName ]; }` — MASQUERADE on packets leaving via any external NIC | | Loopback DROP | `networking.firewall.extraInputRules` — drops bridge-subnet → `127.0.0.0/8` traffic; defence-in-depth against routing table leaks | +| Forge access | `networking.firewall.interfaces..allowedTCPPorts` — opens `forge.httpPort` on the bridge interface so isolated agents can reach forgejo at `:` | +| Forge URL | `HIVE_FORGE_URL` flips from `http://127.0.0.1:3000` to `http://:3000` — forwarded to containers via meta flake | | c0re signal | `HIVE_NETWORK_ISOLATION=1`, `HIVE_NETWORK_BRIDGE`, `HIVE_NETWORK_SUBNET` in `systemd.services.hive-c0re.environment` | `HIVE_NETWORK_SUBNET` is the host-side bridge IP + prefix (e.g. diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 7b89f221..c56d717d 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -557,12 +557,19 @@ in HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarmName; } // lib.optionalAttrs config.services.hyperhive.forge.enable { - # Loopback for in-cluster calls (agents share host netns; - # external `forge.` sub-domain isn't DNS-resolvable - # from inside nspawn). See - # `docs/gateway.md::HIVE_FORGE_URL: loopback for in-cluster, - # sub-domain for the operator`. - HIVE_FORGE_URL = "http://127.0.0.1:${toString config.services.hyperhive.forge.httpPort}"; + # In-cluster forge URL. When containers are isolated (private netns), + # 127.0.0.1 is the container's own loopback — unreachable for host + # services. Use the bridge gateway IP instead; forgejo binds 0.0.0.0 + # so it's reachable there. Shared-netns mode keeps loopback path. + # External `forge.` sub-domain isn't DNS-resolvable from inside + # nspawn either way. See `docs/gateway.md::HIVE_FORGE_URL`. + HIVE_FORGE_URL = + if + config.services.hyperhive.network.enable && config.services.hyperhive.network.isolateContainers + then + "http://${config.services.hyperhive.network.bridgeIp}:${toString config.services.hyperhive.forge.httpPort}" + else + "http://127.0.0.1:${toString config.services.hyperhive.forge.httpPort}"; } // lib.optionalAttrs config.services.hyperhive.matrix.gui.enable { # Availability flags read by the dashboard's `/api/state`. diff --git a/nix/modules/hive-network.nix b/nix/modules/hive-network.nix index 66725bc6..2ee1a211 100644 --- a/nix/modules/hive-network.nix +++ b/nix/modules/hive-network.nix @@ -215,6 +215,17 @@ in ip saddr ${cfg.bridgeIp}/${toString cfg.bridgePrefixLength} ip daddr 127.0.0.0/8 drop ''; + # Allow isolated agents to reach the forge via the bridge gateway IP. + # Forgejo binds 0.0.0.0 so it's reachable at `bridgeIp:httpPort` from + # inside agent containers; without this rule the default INPUT policy + # drops the connection before it reaches forgejo. Only added when forge + # is enabled — no-op otherwise. + networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts = + lib.optionals config.services.hyperhive.forge.enable + [ + config.services.hyperhive.forge.httpPort + ]; + # Tells hive-c0re to pass PRIVATE_NETWORK + bridge settings to each # container. HIVE_NETWORK_SUBNET is host-bridge IP/prefix, not canonical # network address — the Rust side normalises before subnet arithmetic. From 806d0e4a61d910e43594f2b7842425de7eb0bcb8 Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 3 Jun 2026 16:10:32 +0200 Subject: [PATCH 2/3] fix: use forge domain URL + open 80/443 for isolated agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit when isolateContainers=true, isolated agents have dnsmasq as their resolver — forge. resolves to bridgeIp. route HIVE_FORGE_URL through nginx on port 80 instead of exposing the raw forge port. - HIVE_FORGE_URL: http:// when isolated (nginx proxies) - bridge firewall: open 80+443 for agents to reach nginx (gateway) - remove forge-specific httpPort rule (no longer needed) - update docs/gateway.md + docs/network.md per mara's review comment on PR #1150. --- docs/gateway.md | 19 +++++++------------ docs/network.md | 9 +++++++-- nix/modules/hive-c0re.nix | 14 +++++++------- nix/modules/hive-network.nix | 17 +++++++---------- 4 files changed, 28 insertions(+), 31 deletions(-) diff --git a/docs/gateway.md b/docs/gateway.md index ba28f942..2496810f 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -200,27 +200,22 @@ dashboard reach by design — the surface is privileged (approve / deny / destroy) and must not be exposed without a real reverse proxy in front. -## `HIVE_FORGE_URL`: bridge gateway for isolated agents, loopback for shared-netns +## `HIVE_FORGE_URL`: domain via gateway for isolated agents, loopback for shared-netns Agents poll `HIVE_FORGE_URL` for Forgejo notifications + run all `hive-forge` calls against it. `hive-c0re.nix` sets this based on the network isolation mode: -- **`network.isolateContainers = true`**: agents run in private netns, - so host loopback is unreachable. `HIVE_FORGE_URL` is set to - `http://:`. Forgejo binds `0.0.0.0` so it's - reachable at the bridge gateway IP. `hive-network.nix` opens - `forge.httpPort` on the bridge interface automatically. +- **`network.isolateContainers = true`**: agents run in private netns and + get the bridge dnsmasq as their resolver. `HIVE_FORGE_URL` is set to + `http://` (default `forge.`). Agents resolve + the hostname via dnsmasq → bridge IP, then reach nginx on port 80 (bridge + firewall opens 80+443 when isolation is on). nginx proxies to forgejo — the + same path an operator browser takes, no raw port exposure needed. - **`network.isolateContainers = false`** (default): agents share the host's network namespace, so loopback reaches forgejo directly. `HIVE_FORGE_URL` is `http://127.0.0.1:`. -The sub-domain default (`forge.`) is for **operator -browsers + cross-host clients**, not in-cluster traffic. Using the -sub-domain URL inside agent containers would fail every `hive-forge` -invocation with "Name or service not known" — the agent's nspawn -doesn't have DNS for the external hostname. - ## hive-forge container shape Private Forgejo wrapped in a nixos-container (`hive-forge`, not diff --git a/docs/network.md b/docs/network.md index b5f928d7..e0d2013c 100644 --- a/docs/network.md +++ b/docs/network.md @@ -95,6 +95,11 @@ agent containers. interface only. Other interfaces stay closed. The hive resolver isn't an external-facing service. +When `isolateContainers = true`, `allowedTCPPorts` is extended with +`[ 80 443 ]` so isolated agents can reach nginx (gateway container, +shared host netns) for the forge sub-domain, per-agent UI proxies, +and any other HTTP services. + ## Container isolation `services.hyperhive.network.isolateContainers` (default `false`) flips @@ -108,8 +113,8 @@ agent containers from shared host netns to private netns. Set only after | IP forwarding | `boot.kernel.sysctl."net.ipv4.ip_forward" = 1` | | Internet NAT | `networking.nat { enable = true; internalInterfaces = [ bridgeName ]; }` — MASQUERADE on packets leaving via any external NIC | | Loopback DROP | `networking.firewall.extraInputRules` — drops bridge-subnet → `127.0.0.0/8` traffic; defence-in-depth against routing table leaks | -| Forge access | `networking.firewall.interfaces..allowedTCPPorts` — opens `forge.httpPort` on the bridge interface so isolated agents can reach forgejo at `:` | -| Forge URL | `HIVE_FORGE_URL` flips from `http://127.0.0.1:3000` to `http://:3000` — forwarded to containers via meta flake | +| Gateway access | `networking.firewall.interfaces..allowedTCPPorts = [ 80 443 ]` — lets isolated agents reach nginx on the host (shared netns) | +| Forge URL | `HIVE_FORGE_URL` flips from `http://127.0.0.1:3000` to `http://forge.` — agents resolve via dnsmasq, nginx proxies to forgejo | | c0re signal | `HIVE_NETWORK_ISOLATION=1`, `HIVE_NETWORK_BRIDGE`, `HIVE_NETWORK_SUBNET` in `systemd.services.hive-c0re.environment` | `HIVE_NETWORK_SUBNET` is the host-side bridge IP + prefix (e.g. diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index c56d717d..b98f9b58 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -557,17 +557,17 @@ in HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarmName; } // lib.optionalAttrs config.services.hyperhive.forge.enable { - # In-cluster forge URL. When containers are isolated (private netns), - # 127.0.0.1 is the container's own loopback — unreachable for host - # services. Use the bridge gateway IP instead; forgejo binds 0.0.0.0 - # so it's reachable there. Shared-netns mode keeps loopback path. - # External `forge.` sub-domain isn't DNS-resolvable from inside - # nspawn either way. See `docs/gateway.md::HIVE_FORGE_URL`. + # In-cluster forge URL. + # - Isolated (private netns): containers resolve `forge.` via + # the bridge dnsmasq and reach nginx on port 80. No raw forge port + # needed — nginx proxies to forgejo as it does for the operator. + # - Shared netns: host loopback is reachable, use direct port. + # See `docs/gateway.md::HIVE_FORGE_URL`. HIVE_FORGE_URL = if config.services.hyperhive.network.enable && config.services.hyperhive.network.isolateContainers then - "http://${config.services.hyperhive.network.bridgeIp}:${toString config.services.hyperhive.forge.httpPort}" + "http://${config.services.hyperhive.forge.domain}" else "http://127.0.0.1:${toString config.services.hyperhive.forge.httpPort}"; } diff --git a/nix/modules/hive-network.nix b/nix/modules/hive-network.nix index 2ee1a211..69298e4f 100644 --- a/nix/modules/hive-network.nix +++ b/nix/modules/hive-network.nix @@ -215,16 +215,13 @@ in ip saddr ${cfg.bridgeIp}/${toString cfg.bridgePrefixLength} ip daddr 127.0.0.0/8 drop ''; - # Allow isolated agents to reach the forge via the bridge gateway IP. - # Forgejo binds 0.0.0.0 so it's reachable at `bridgeIp:httpPort` from - # inside agent containers; without this rule the default INPUT policy - # drops the connection before it reaches forgejo. Only added when forge - # is enabled — no-op otherwise. - networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts = - lib.optionals config.services.hyperhive.forge.enable - [ - config.services.hyperhive.forge.httpPort - ]; + # Allow isolated agents to reach the gateway (nginx on the host, shared + # netns). Port 80 covers `http://forge.`, per-agent UI proxies, + # and any other HTTP services the gateway fronts. Port 443 for HTTPS. + networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts = [ + 80 + 443 + ]; # Tells hive-c0re to pass PRIVATE_NETWORK + bridge settings to each # container. HIVE_NETWORK_SUBNET is host-bridge IP/prefix, not canonical From a2a96490d3248c63fc87a10b935f65708faf717e Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 3 Jun 2026 16:13:03 +0200 Subject: [PATCH 3/3] fix: assert gateway.enable when isolateContainers + forge.enable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isolated agents reach forge via http://forge. → nginx. without the gateway there is nothing on port 80 to serve that hostname. assert early rather than fail silently at runtime. addresses argus yellow note on PR #1150. --- nix/modules/hive-network.nix | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/nix/modules/hive-network.nix b/nix/modules/hive-network.nix index 69298e4f..1cce485a 100644 --- a/nix/modules/hive-network.nix +++ b/nix/modules/hive-network.nix @@ -196,6 +196,18 @@ in resolver must be running before isolation is flipped on). ''; } + { + assertion = + !config.services.hyperhive.forge.enable || config.services.hyperhive.gateway.enable; + message = '' + services.hyperhive.network.isolateContainers = true with + services.hyperhive.forge.enable = true requires + services.hyperhive.gateway.enable = true — isolated agents + reach the forge via `http://forge.` which nginx (in + the gateway container) proxies to forgejo. Without the gateway + there is nothing listening on port 80 to serve that hostname. + ''; + } ]; })