fix(#2310): hive-ci private network — bridge-connected, no host-loopback reach
Give hive-ci its own network namespace (privateNetwork=true, hostBridge) instead of sharing the host netns. This closes the path by which a prompt-injected PR nix build could reach the unauthenticated dashboard at 127.0.0.1:7000. The runner now reaches the forge through the gateway: - networking.extraHosts maps forgeCfg.domain → bridgeIp - runner url = http://${forgeCfg.domain} (gateway port 80, addSSL=true, no HTTP→HTTPS redirect — HTTP is served alongside HTTPS) - networking.useDHCP = true for the bridge-attached veth - networking.nameservers = [bridgeIp] (dnsmasq on bridge, external fwd) Ports 80 and 443 are already open on the bridge firewall (hive-network.nix). The bridge→127.0.0.0/8 DROP rule (defence-in-depth) is unchanged — CI can no longer reach 127.0.0.1:{7000 dashboard, forge httpPort, etc.}. Requires forge.behindGateway=true (the default when hyperhive is enabled) — asserted so misconfigured hosts get a clear build error. Note: the host-side hive-ci-prefetch script still uses 127.0.0.1:${httpPort} directly (it runs on the host, not in the container) — no change needed there.
This commit is contained in:
parent
476a7a3c9f
commit
056a442a18
1 changed files with 61 additions and 25 deletions
|
|
@ -8,6 +8,7 @@ let
|
|||
cfg = config.services.hyperhive.forge.ci;
|
||||
forgeCfg = config.services.hyperhive.forge;
|
||||
gatewayCfg = config.services.hyperhive.gateway;
|
||||
networkCfg = config.services.hyperhive.network;
|
||||
tlsCfg = config.services.hyperhive.tls;
|
||||
|
||||
# Self-signed TLS is the gateway default (no operator cert / ACME). When
|
||||
|
|
@ -166,8 +167,14 @@ let
|
|||
in
|
||||
{
|
||||
# Forgejo Actions runner in a `hive-ci` nixos-container.
|
||||
# Shares host netns (same as hive-forge), so the runner reaches
|
||||
# the forge at `http://127.0.0.1:<httpPort>` without extra plumbing.
|
||||
# Uses a private network namespace (bridge-connected, not host netns)
|
||||
# so CI build scripts cannot reach host-loopback services (dashboard,
|
||||
# forge internal port, etc.) — a key defence against prompt-injection
|
||||
# via PR nix builds. The runner reaches the forge via the
|
||||
# gateway at `http://${forgeCfg.domain}` (resolved to the bridge IP
|
||||
# via `networking.extraHosts`; gateway port 80 is always open on the
|
||||
# bridge; `addSSL = true` means HTTP is served alongside HTTPS without
|
||||
# a redirect). See docs/network.md.
|
||||
# Container is non-ephemeral: the runner's registered credentials
|
||||
# survive restarts (gitea-actions-runner writes them to its stateDir
|
||||
# on first registration and reuses them on every subsequent start).
|
||||
|
|
@ -265,9 +272,24 @@ in
|
|||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# No forge-presence assertion needed: the internal forge is mandatory
|
||||
# (deploys with hyperhive), so the runner always has an instance to
|
||||
# register against.
|
||||
# `forge.behindGateway = true` (the default) is required because the
|
||||
# CI container uses private networking and reaches the forge through
|
||||
# the gateway vhost. Without the gateway vhost there is no HTTP
|
||||
# listener for `forgeCfg.domain` on the bridge that the runner can
|
||||
# connect to.
|
||||
assertions = [
|
||||
{
|
||||
assertion = forgeCfg.behindGateway;
|
||||
message = ''
|
||||
services.hyperhive.forge.ci.enable requires
|
||||
services.hyperhive.forge.behindGateway = true.
|
||||
The CI container runs with a private network namespace and
|
||||
reaches the forge through the gateway vhost on the bridge IP.
|
||||
Set behindGateway = true (it defaults to true alongside
|
||||
services.hyperhive.enable).
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
# Create /run/hive-ci/ on the host and seed runner-token with a
|
||||
# placeholder. hive-ci-prefetch.service overwrites it with the real
|
||||
|
|
@ -337,8 +359,13 @@ in
|
|||
containers.hive-ci = {
|
||||
autoStart = true;
|
||||
ephemeral = false;
|
||||
# Shared host netns: runner reaches hive-forge at localhost.
|
||||
privateNetwork = false;
|
||||
# Private network namespace, attached to the hive bridge so the
|
||||
# runner reaches the forge via the gateway — and cannot reach
|
||||
# host-loopback (127.0.0.1:7000 dashboard, raw forge port, etc.).
|
||||
# Requires `forge.behindGateway = true` (asserted in the options
|
||||
# block above). See docs/network.md.
|
||||
privateNetwork = true;
|
||||
hostBridge = networkCfg.bridgeName;
|
||||
|
||||
bindMounts = {
|
||||
# Pre-filled by hive-ci-prefetch.service (host-side) before the
|
||||
|
|
@ -367,23 +394,27 @@ in
|
|||
{
|
||||
system.stateVersion = "26.05";
|
||||
|
||||
# Resolve the hive's own forge domain to loopback inside the
|
||||
# runner. The Forgejo Actions artifact API hands the runner an
|
||||
# upload URL built from forgejo's ROOT_URL — the *public* forge
|
||||
# domain (`forge.<domain>`), not the runner's
|
||||
# `127.0.0.1:<httpPort>` registration URL. This container shares
|
||||
# host netns but carries no resolver entry for the hive's own
|
||||
# domains (the gateway's `localHostsEntry` only touches the host),
|
||||
# so `actions/upload-artifact` dies with
|
||||
# Error: getaddrinfo ENOTFOUND forge.<domain>
|
||||
# while normal CI is unaffected (checkout uses the localhost
|
||||
# registration URL). Map the forge domain to loopback so the
|
||||
# upload reaches the local forge — via the gateway on :80 when
|
||||
# behindGateway, or forge directly on :httpPort otherwise. The
|
||||
# gateway proxies all of `/` (artifact endpoints included), and a
|
||||
# `localhost` domain (no hive-domain set) is already loopback, so
|
||||
# this is a harmless no-op in that case.
|
||||
networking.extraHosts = "127.0.0.1 ${forgeCfg.domain}";
|
||||
# Point the forge domain at the bridge IP so the runner can
|
||||
# reach the forge through the gateway — both for registration /
|
||||
# polling (runner URL below) and for artifact uploads (the
|
||||
# Forgejo Actions artifact API uses ROOT_URL, i.e. the public
|
||||
# forge domain, not a localhost URL). The gateway vhost for
|
||||
# `forgeCfg.domain` proxies all `/` → forge; `addSSL = true`
|
||||
# means HTTP:80 is served without redirect alongside HTTPS:443.
|
||||
# Ports 80 and 443 are always open on the bridge firewall (see
|
||||
# hive-network.nix). No DNS lookup needed — /etc/hosts wins.
|
||||
networking.extraHosts = "${networkCfg.bridgeIp} ${forgeCfg.domain}";
|
||||
# DNS: use the hive resolver on the bridge IP (dnsmasq in
|
||||
# hive-gateway) for external lookups (git checkout, crate
|
||||
# registries, etc.). The bridge→loopback DROP rule does not
|
||||
# affect traffic destined for the bridge IP itself.
|
||||
networking.nameservers = [ networkCfg.bridgeIp ];
|
||||
# With privateNetwork=true + hostBridge the container's veth
|
||||
# is bridge-attached. Enable DHCP so the container gets an IP
|
||||
# from dnsmasq on the bridge (hive-gateway serves the bridge
|
||||
# subnet). Matches how normal hive agent containers acquire
|
||||
# their addresses.
|
||||
networking.useDHCP = true;
|
||||
|
||||
# nspawn containers can't create user-namespaces, so nix
|
||||
# sandboxing always fails. Fall back to unsandboxed builds.
|
||||
|
|
@ -407,7 +438,12 @@ in
|
|||
services.gitea-actions-runner.instances.hive = {
|
||||
enable = true;
|
||||
name = cfg.name;
|
||||
url = "http://127.0.0.1:${toString forgeCfg.httpPort}";
|
||||
# Route through the gateway (bridge IP, port 80) so the
|
||||
# runner never touches host-loopback. The forge domain
|
||||
# resolves to the bridge IP via networking.extraHosts above;
|
||||
# the gateway vhost `forgeCfg.domain` proxies to the forge
|
||||
# on HTTP:80 (addSSL=true, no HTTP→HTTPS redirect).
|
||||
url = "http://${forgeCfg.domain}";
|
||||
# EnvironmentFile providing TOKEN= — pre-filled by the
|
||||
# host-side hive-ci-prefetch.service before the container
|
||||
# starts; bind-mounted read-only from /run/hive-ci/runner-token
|
||||
|
|
|
|||
Loading…
Reference in a new issue