c0re: bind dashboard to 127.0.0.1 only (#652)

This commit is contained in:
damocles 2026-05-30 19:29:27 +02:00 committed by Mara
commit c5d466c5c5
2 changed files with 26 additions and 12 deletions

View file

@ -92,9 +92,16 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
// /static/dashboard.css → dist/static/dashboard.css, etc.).
.fallback_service(ServeDir::new(&static_dir))
.with_state(AppState { coord });
let addr = SocketAddr::from(([0, 0, 0, 0], port));
// Bind loopback-only (#652). External access funnels through
// hive-gateway (in-host-netns nginx container), which proxies
// `/` → `127.0.0.1:<dashboardPort>` upstream. Operators who opt
// out of the gateway lose remote dashboard access — that's by
// design; the c0re HTTP surface is privileged (approve / deny /
// destroy, etc.) and any external exposure needs to pass through
// a real reverse proxy with auth.
let addr = SocketAddr::from(([127, 0, 0, 1], port));
let listener = bind_with_retry(addr).await?;
tracing::info!(%port, "dashboard listening");
tracing::info!(%addr, "dashboard listening");
axum::serve(listener, app).await?;
Ok(())
}

View file

@ -194,21 +194,28 @@ in
managerToplevel
];
# Dashboard + per-container web UIs share the host's network namespace and
# need their ports reachable when there's no gateway in front. Dashboard:
# `cfg.dashboardPort` (default 7000). Manager: 8000. Sub-agents: 8100..8999
# (deterministic hash; see `lifecycle::agent_web_port`).
# Per-container web UIs share the host's network namespace and need
# their ports reachable when there's no gateway in front. Manager:
# 8000. Sub-agents: 8100..8999 (deterministic hash; see
# `lifecycle::agent_web_port`).
#
# The dashboard port (`cfg.dashboardPort`, default 7000) is *not*
# listed here — since #652 the dashboard binds `127.0.0.1` only,
# so opening the firewall hole would be a no-op. Remote dashboard
# access flows through hive-gateway (default-on); operators who
# opt out of the gateway lose external dashboard reach by design —
# the surface is privileged (approve / deny / destroy) and must
# not be exposed without a real reverse proxy in front.
#
# When `services.hyperhive.gateway.enable = true` (the default), the
# gateway nginx is the sole external entry point and proxies to
# `127.0.0.1:7000` etc. internally — leaving the direct ports open
# in the host firewall would defeat the gateway's "single front
# door" story (closes #621). Operators who opt out of the gateway
# still get the direct ports opened so the legacy
# `http://<host>:7000/` flow works.
# `127.0.0.1:7000` etc. internally — leaving the per-agent ports
# open in the host firewall would defeat the gateway's "single
# front door" story (closes #621). Operators who opt out of the
# gateway still get those direct ports opened so the legacy
# `http://<host>:8100/` flow works.
networking.firewall = lib.mkIf (!config.services.hyperhive.gateway.enable) {
allowedTCPPorts = [
cfg.dashboardPort
8000
];
allowedTCPPortRanges = [