fix(#1977): exposeHostPorts is firewall-only (drop conflicting loopback proxy)
This commit is contained in:
parent
fe68bfbadc
commit
609438a889
2 changed files with 45 additions and 86 deletions
|
|
@ -100,26 +100,28 @@ When `isolateContainers = true`, `allowedTCPPorts` is extended with
|
||||||
shared host netns) for the forge sub-domain, per-agent UI proxies,
|
shared host netns) for the forge sub-domain, per-agent UI proxies,
|
||||||
and any other HTTP services.
|
and any other HTTP services.
|
||||||
|
|
||||||
### Reaching host-loopback services (`exposeHostPorts`)
|
### Reaching host services (`exposeHostPorts`)
|
||||||
|
|
||||||
Agents are deliberately cut off from the host loopback (the bridge→
|
By default agents can only reach the host on 80/443 (+53 DNS), so a
|
||||||
`127.0.0.0/8` DROP rule below), so a service that only binds
|
host-side service on another port — e.g. a dev OTEL collector for
|
||||||
`127.0.0.1` on the host — e.g. a dev OTEL collector for
|
`services.hyperhive.otel.endpoint` — is unreachable.
|
||||||
`services.hyperhive.otel.endpoint` — is unreachable by default.
|
|
||||||
|
|
||||||
`services.hyperhive.network.exposeHostPorts = [ 4318 ];` opens a
|
`services.hyperhive.network.exposeHostPorts = [ 4318 ];` opens each
|
||||||
controlled path for each listed TCP port `P`:
|
listed TCP port `P` on the bridge-interface `allowedTCPPorts`, so an
|
||||||
|
agent can connect to `<bridgeIp>:P` (point the collector endpoint at
|
||||||
|
`http://<bridgeIp>:4318`, default `http://10.42.0.1:4318`).
|
||||||
|
|
||||||
- a socket-activated `systemd-socket-proxyd` (`hive-hostport-<P>`)
|
This is **firewall-only**: the host service must bind an address
|
||||||
listens on `<bridgeIp>:P` and forwards to `127.0.0.1:P`;
|
reachable from the bridge — `0.0.0.0` or the bridge IP — not loopback
|
||||||
- `P` is added to the bridge-interface `allowedTCPPorts`.
|
only. The bridge→`127.0.0.0/8` DROP rule (below) is unchanged, so a
|
||||||
|
service bound to `127.0.0.1` only stays unreachable; rebind it to
|
||||||
|
`0.0.0.0`. (An earlier revision shipped a per-port
|
||||||
|
`systemd-socket-proxyd` bridge→loopback forwarder, but that collides
|
||||||
|
EADDRINUSE with any collector already bound to `0.0.0.0` — which is the
|
||||||
|
common case — so the proxy was dropped in favour of opening the port.)
|
||||||
|
|
||||||
The agent then points at `http://<bridgeIp>:P` (default
|
The port is reachable by **every** agent on the bridge subnet (like
|
||||||
`http://10.42.0.1:4318`). This keeps the loopback DROP rule intact:
|
DNS/gateway), so only expose services safe for any agent to reach.
|
||||||
agents only ever connect to the bridge IP, and the host's own proxy
|
|
||||||
process is what dials `127.0.0.1`. The port is reachable by **every**
|
|
||||||
agent on the bridge subnet (like DNS/gateway), so only expose services
|
|
||||||
safe for any agent to reach.
|
|
||||||
|
|
||||||
## Container isolation
|
## Container isolation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,22 +97,24 @@ in
|
||||||
default = [ ];
|
default = [ ];
|
||||||
example = [ 4318 ];
|
example = [ 4318 ];
|
||||||
description = ''
|
description = ''
|
||||||
TCP ports on the host's loopback (`127.0.0.1`) to expose to agent
|
TCP ports on the host that agent containers may reach at the bridge
|
||||||
containers at the bridge IP (`bridgeIp`). For each port `P`, a
|
IP (`bridgeIp`). Each listed port `P` is opened on the bridge-interface
|
||||||
socket-activated `systemd-socket-proxyd` listens on
|
firewall, so an agent can connect to `''${bridgeIp}:P` (default
|
||||||
`''${bridgeIp}:P` and forwards to `127.0.0.1:P`, and `P` is opened
|
`10.42.0.1:P`).
|
||||||
on the bridge-interface firewall.
|
|
||||||
|
|
||||||
Use this to let agents reach a host-local service that only binds
|
Use this to let agents reach a host-local service — e.g. an
|
||||||
loopback — e.g. an OpenTelemetry collector for
|
OpenTelemetry collector for `services.hyperhive.otel.endpoint` (set
|
||||||
`services.hyperhive.otel.endpoint`. The agent points at
|
`endpoint = "http://''${bridgeIp}:P"`).
|
||||||
`http://''${bridgeIp}:P` (default `http://10.42.0.1:P`).
|
|
||||||
|
|
||||||
This does NOT weaken the bridge→loopback DROP rule (defence-in-depth):
|
**The host service must bind an address reachable from the bridge** —
|
||||||
agents never reach `127.0.0.0/8`; they connect to the bridge IP and
|
`0.0.0.0` or the bridge IP (`bridgeIp`) — not loopback-only. The
|
||||||
the host's own proxy process dials loopback. The exposed port is
|
bridge→`127.0.0.0/8` DROP rule (defence-in-depth) is unchanged: this
|
||||||
reachable by EVERY agent on the bridge subnet (same as DNS/gateway),
|
only opens the firewall, it does not bridge loopback. A service that
|
||||||
so only expose services that are safe for any agent to reach.
|
binds `127.0.0.1` only is still unreachable; rebind it to `0.0.0.0`.
|
||||||
|
|
||||||
|
The exposed port is reachable by EVERY agent on the bridge subnet
|
||||||
|
(same as DNS/gateway), so only expose services safe for any agent to
|
||||||
|
reach.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -246,60 +248,15 @@ in
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
# Host-loopback port exposure: one socket-activated systemd-socket-proxyd
|
# Host port exposure: open each `exposeHostPorts` entry on the bridge
|
||||||
# per `exposeHostPorts` entry, listening on the bridge IP and forwarding to
|
# firewall so agents can reach a host service at `<bridgeIp>:P`. The host
|
||||||
# the host's loopback. This is how agents reach a host-local service (e.g. a
|
# service must bind `0.0.0.0` or the bridge IP (a loopback-only bind stays
|
||||||
# dev OTEL collector on 127.0.0.1) without weakening the bridge→loopback
|
# unreachable — the bridge→127.0.0.0/8 DROP rule above is unchanged). This
|
||||||
# DROP rule above — agents connect to the bridge IP, and the host's own
|
# is firewall-only by design: a host service that binds `0.0.0.0` already
|
||||||
# proxy process is what dials 127.0.0.1. See docs/network.md.
|
# serves the bridge IP, so an extra bridge-IP proxy would only collide
|
||||||
|
# (EADDRINUSE) with it. Merges with the [ 80 443 ] gateway ports above.
|
||||||
(lib.mkIf (config.services.hyperhive.enable && cfg.exposeHostPorts != [ ]) {
|
(lib.mkIf (config.services.hyperhive.enable && cfg.exposeHostPorts != [ ]) {
|
||||||
# Open the proxied ports on the bridge firewall HERE — same block + same
|
|
||||||
# gate as the proxies — so the firewall hole and the listener are always
|
|
||||||
# created together (never one without the other). Merges with the
|
|
||||||
# [ 80 443 ] gateway ports defined above. Independent of the deprecated
|
|
||||||
# isolateContainers toggle: isolation is unconditional, so this works
|
|
||||||
# under network isolation (the only mode).
|
|
||||||
networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts = cfg.exposeHostPorts;
|
networking.firewall.interfaces.${cfg.bridgeName}.allowedTCPPorts = cfg.exposeHostPorts;
|
||||||
|
|
||||||
systemd.sockets = lib.listToAttrs (
|
|
||||||
map (
|
|
||||||
p:
|
|
||||||
lib.nameValuePair "hive-hostport-${toString p}" {
|
|
||||||
description = "Host-loopback proxy socket for port ${toString p} (bridge→127.0.0.1)";
|
|
||||||
wantedBy = [ "sockets.target" ];
|
|
||||||
socketConfig.ListenStream = "${cfg.bridgeIp}:${toString p}";
|
|
||||||
}
|
|
||||||
) cfg.exposeHostPorts
|
|
||||||
);
|
|
||||||
|
|
||||||
systemd.services = lib.listToAttrs (
|
|
||||||
map (
|
|
||||||
p:
|
|
||||||
lib.nameValuePair "hive-hostport-${toString p}" {
|
|
||||||
description = "Host-loopback proxy for port ${toString p} (bridge→127.0.0.1)";
|
|
||||||
requires = [ "hive-hostport-${toString p}.socket" ];
|
|
||||||
after = [ "hive-hostport-${toString p}.socket" ];
|
|
||||||
serviceConfig = {
|
|
||||||
ExecStart = "${config.systemd.package}/lib/systemd/systemd-socket-proxyd 127.0.0.1:${toString p}";
|
|
||||||
SyslogIdentifier = "hive-hostport-${toString p}";
|
|
||||||
# Hardened userspace TCP relay. AF_INET/AF_INET6 for the upstream
|
|
||||||
# loopback dial; AF_UNIX only for systemd-socket-proxyd's sd_notify
|
|
||||||
# (the listen fd itself is inherited via LISTEN_FDS, so the relay
|
|
||||||
# never socket()s its own listener). DynamicUser keeps it unprivileged.
|
|
||||||
DynamicUser = true;
|
|
||||||
NoNewPrivileges = true;
|
|
||||||
ProtectSystem = "strict";
|
|
||||||
ProtectHome = true;
|
|
||||||
PrivateTmp = true;
|
|
||||||
RestrictAddressFamilies = [
|
|
||||||
"AF_INET"
|
|
||||||
"AF_INET6"
|
|
||||||
"AF_UNIX"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
) cfg.exposeHostPorts
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# Deprecation surface for the removed toggles. Both options are kept so
|
# Deprecation surface for the removed toggles. Both options are kept so
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue