What is wrong
modules/halogen.nix:516 opens the Halogen API port on the LAN interface for every host that sets services.halogen.enable, with no gate:
networking.firewall.interfaces.${cfg.lanInterface}.allowedTCPPorts = [ cfg.port ];
modules/strix.nix:68-72 declares Halogen on both twins and splits only autoStart:
enable = true;
autoStart = config.networking.hostName == "worker";
lanInterface = if config.networking.hostName == "coordinator" then "wlp192s0" else "enp191s0";
So the coordinator — which by design serves no model — still opens :8731 on wlp192s0, the Freebox wifi uplink.
The API has no authentication. modules/halogen.nix:46-48 states the safety argument for the door:
The API (OpenAI-compatible on :8731, /health for liveness) has NO authentication. It is published on the host network and admitted on the LAN interface only; every client on that segment is a pinned house device.
That argument holds for the worker's enp191s0 (wired into the BE550, a pinned-device segment). It does not transfer to the coordinator's wlp192s0, which is the wifi uplink, not the BE550 wired segment.
This is already asserted against, and the assert is failing
flake.nix:1695, in checks.nas-topology:
# The coordinator serves no model: no inference door on its LAN leg.
assert !(builtins.elem 8731 coordinator.networking.firewall.interfaces.wlp192s0.allowedTCPPorts);
nix flake check --offline --no-build fails on it today:
error: assertion '(! ((builtins).elem 8731 (coordinator).networking.firewall.interfaces.wlp192s0.allowedTCPPorts))' failed
at .../flake.nix:1695:11
Reproduced on the committed tree at 8955c9e9 (not a working-tree artifact). Evaluated value:
$ nix eval --json .#nixosConfigurations.coordinator.config.networking.firewall.interfaces.wlp192s0.allowedTCPPorts
[80,443,8731]
checks.nas-topology is the first failing check in the run, so it also masks every check evaluated after it.
Live state on the coordinator
The door is real, not just declared:
$ sudo iptables -S | grep 8731
-A nixos-fw -i wlp192s0 -p tcp -m tcp --dport 8731 -j nixos-fw-accept
$ ss -lntp | grep 8731
(nothing listening)
$ systemctl is-active podman-halogen-flash.service
inactive
So today it is an open door to an empty room. The exposure is latent, not live: the moment an operator runs halogen-switch flash or halogen-switch qwen38-27b on the coordinator — the documented, intended way to use Halogen there — an unauthenticated OpenAI-compatible inference endpoint is reachable from the wifi segment for as long as the model is resident.
Where it came from
The 2026-09-16 change that declared Halogen on both twins (services.halogen.enable = true on the coordinator, with autoStart = false, so halogen-switch can start it on demand). The firewall line keys off cfg.enable, so "declare the server here" silently also meant "admit the LAN".
Nothing about the 2026-09-16 intent asked for the coordinator's door: modules/strix.nix:60-61 is explicit that the worker "is the fleet's utility endpoint (http://worker:8731)" and that on the coordinator "nothing starts there at boot".
Proposed fix
Split the door from the declaration. Add an explicit services.halogen.openLanPort, defaulting to autoStart, and gate line 516 on it:
- worker (
autoStart = true) keeps its door — no change, it is the fleet endpoint;
- coordinator (
autoStart = false) loses a door it was never meant to have;
- an operator who genuinely wants to serve the fleet from the coordinator sets one option, in the open, instead of getting it as a side effect of
enable.
Coordinator-local use via halogen-switch is unaffected: the container publishes on the host network and loopback is never filtered, so http://localhost:8731 keeps working with the LAN door shut.
The existing flake.nix:1695 assert is the regression test and needs no change.
What is wrong
modules/halogen.nix:516opens the Halogen API port on the LAN interface for every host that setsservices.halogen.enable, with no gate:modules/strix.nix:68-72declares Halogen on both twins and splits onlyautoStart:So the coordinator — which by design serves no model — still opens
:8731on wlp192s0, the Freebox wifi uplink.The API has no authentication.
modules/halogen.nix:46-48states the safety argument for the door:That argument holds for the worker's
enp191s0(wired into the BE550, a pinned-device segment). It does not transfer to the coordinator'swlp192s0, which is the wifi uplink, not the BE550 wired segment.This is already asserted against, and the assert is failing
flake.nix:1695, inchecks.nas-topology:nix flake check --offline --no-buildfails on it today:Reproduced on the committed tree at
8955c9e9(not a working-tree artifact). Evaluated value:checks.nas-topologyis the first failing check in the run, so it also masks every check evaluated after it.Live state on the coordinator
The door is real, not just declared:
So today it is an open door to an empty room. The exposure is latent, not live: the moment an operator runs
halogen-switch flashorhalogen-switch qwen38-27bon the coordinator — the documented, intended way to use Halogen there — an unauthenticated OpenAI-compatible inference endpoint is reachable from the wifi segment for as long as the model is resident.Where it came from
The 2026-09-16 change that declared Halogen on both twins (
services.halogen.enable = trueon the coordinator, withautoStart = false, sohalogen-switchcan start it on demand). The firewall line keys offcfg.enable, so "declare the server here" silently also meant "admit the LAN".Nothing about the 2026-09-16 intent asked for the coordinator's door:
modules/strix.nix:60-61is explicit that the worker "is the fleet'sutilityendpoint (http://worker:8731)" and that on the coordinator "nothing starts there at boot".Proposed fix
Split the door from the declaration. Add an explicit
services.halogen.openLanPort, defaulting toautoStart, and gate line 516 on it:autoStart = true) keeps its door — no change, it is the fleet endpoint;autoStart = false) loses a door it was never meant to have;enable.Coordinator-local use via
halogen-switchis unaffected: the container publishes on the host network and loopback is never filtered, sohttp://localhost:8731keeps working with the LAN door shut.The existing
flake.nix:1695assert is the regression test and needs no change.