refactor(gateway): make the gateway unconditional — remove gateway.enable

The gateway container starts alongside every hyperhive deployment, so
gating it behind a separate enable flag was a footgun: an operator who
set it false lost the only thing exposed to the outside while the
agent containers kept running. Re-gate the gateway config on the
top-level services.hyperhive.enable instead.

- hive-gateway.nix: drop the gateway.enable mkOption; gate the config
  block on config.services.hyperhive.enable.
- hive-forge.nix: behindGateway now defaults to services.hyperhive.enable;
  remove the behindGateway-requires-gateway assertion (now vacuous).
- hive-network.nix: remove both gateway.enable assertions (vacuous).
- hive-c0re.nix: drop the firewall.allowedTCPPortRanges 8100-8999
  fallback that opened agent ports when the gateway was off (the
  gateway is now the sole entry point); HIVE_GATEWAY_ENABLED is always
  set since the gateway always runs.
- nix/docs/default.nix: remove the gateway.enable = mkForce false stub
  (would be an eval error against the removed option; the gateway is
  already re-gated on hyperhive.enable, which docs force false).
- hive-matrix.nix, dashboard.rs: comment/prose updates only.

BREAKING: operators relying on services.hyperhive.gateway.enable = false
to suppress the gateway must instead point their own reverse proxy at
the gateway's port. NixOS errors clearly on the now-unknown option.
This commit is contained in:
atlas 2026-06-08 23:44:58 +02:00 committed by mara
commit fdf05c1673
7 changed files with 34 additions and 86 deletions

View file

@ -229,15 +229,15 @@ struct StateSnapshot {
/// chrome so the `M4TR1X →` tab doesn't flash when the GUI is off.
matrix_gui_enabled: bool,
/// Whether `hive-gateway` is in front of this dashboard. Sourced
/// from `HIVE_GATEWAY_ENABLED` env var (set by the c0re NixOS
/// module when `services.hyperhive.gateway.enable` is on). When
/// true the dashboard frontend builds same-origin
/// `/agent/<name>/` links to the per-agent web UI (the gateway
/// routes them via the runtime-generated `agents.conf` include
/// file — see `gateway_nginx.rs`); when false it falls back to
/// direct `http://<hostname>:<port>/` TCP links so gateway-off /
/// local-dev deploys keep working. See `docs/gateway.md::Vhost
/// map`.
/// from the `HIVE_GATEWAY_ENABLED` env var, which the c0re NixOS
/// module now always sets (the gateway runs unconditionally
/// alongside hyperhive), so this is effectively always true: the
/// dashboard frontend builds same-origin `/agent/<name>/` links to
/// the per-agent web UI (the gateway routes them via the
/// runtime-generated `agents.conf` include file — see
/// `gateway_nginx.rs`). The `false` branch (direct
/// `http://<hostname>:<port>/` TCP links) is retained as a defensive
/// fallback for the env being unset. See `docs/gateway.md::Vhost map`.
gateway_enabled: bool,
/// Public URL of the forge vhost served by hive-gateway (e.g.
/// `"https://forge.pr1ma.darkest.space"`). Sourced from the
@ -480,7 +480,7 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
gateway_enabled: std::env::var_os("HIVE_GATEWAY_ENABLED").is_some_and(|v| {
// Same truthy-string parse as `matrix_gui_enabled`; the
// env var is set by the c0re NixOS module to the literal
// "1" when `services.hyperhive.gateway.enable` is on.
// "1" — the gateway always runs alongside hyperhive.
let s = v.to_string_lossy().to_ascii_lowercase();
matches!(s.as_str(), "1" | "true" | "yes")
}),

View file

@ -31,7 +31,6 @@ let
services.hyperhive.enable = lib.mkForce false;
services.hyperhive.forge.enable = lib.mkForce false;
services.hyperhive.matrix.enable = lib.mkForce false;
services.hyperhive.gateway.enable = lib.mkForce false;
}
)
];

View file

@ -634,15 +634,9 @@ in
};
users.groups.hive-core = { };
# Open the per-agent web-port range when the gateway is *off* —
# otherwise the gateway nginx is the sole external entry point.
# See `docs/gateway.md::Firewall posture (host-level)`.
networking.firewall.allowedTCPPortRanges = lib.mkIf (!config.services.hyperhive.gateway.enable) [
{
from = 8100;
to = 8999;
}
];
# The gateway nginx is always the sole external entry point (it runs
# alongside hyperhive), so the per-agent web-port range stays closed on
# the host firewall. See `docs/gateway.md::Firewall posture (host-level)`.
# WireGuard inter-hive mesh. Enabled when
# `services.hyperhive.swarm.wireguard.enable = true`. Brings up a
@ -777,9 +771,11 @@ in
# docs/gateway.md::Vhost map.
HIVE_MATRIX_GUI_ENABLED = "1";
}
// lib.optionalAttrs config.services.hyperhive.gateway.enable {
# When true the dashboard builds same-origin `/agent/<name>/`
# links; when false it falls back to direct `<host>:<port>` TCP.
// {
# The gateway always runs, so the dashboard always builds
# same-origin `/agent/<name>/` links (never the direct
# `<host>:<port>` TCP fallback). Kept as an env flag so the
# dashboard doesn't need to learn the gateway is unconditional.
HIVE_GATEWAY_ENABLED = "1";
}
//

View file

@ -112,8 +112,8 @@ in
behindGateway = lib.mkOption {
type = lib.types.bool;
default = gatewayCfg.enable or false;
defaultText = lib.literalExpression "config.services.hyperhive.gateway.enable";
default = config.services.hyperhive.enable;
defaultText = lib.literalExpression "config.services.hyperhive.enable";
description = ''
Serve forgejo through the hive-gateway nginx as a sub-domain
vhost (`server_name = cfg.domain`) instead of directly on
@ -127,9 +127,9 @@ in
- `gateway.localHostsEntry = true` extends `/etc/hosts` to
include `cfg.domain 127.0.0.1` for local dev.
Defaults to `services.hyperhive.gateway.enable` flipping
the gateway on/off auto-routes forge through it. Set `false`
explicitly to keep forge on the direct port even when the
Defaults to `services.hyperhive.enable` (the gateway always runs
alongside hyperhive, so forge auto-routes through it). Set `false`
explicitly to keep forge on the direct port even though the
gateway is running (e.g. an external git client that doesn't
traverse the gateway).
@ -211,21 +211,6 @@ in
or "git.internal".
'';
}
{
# behindGateway requires the gateway module to actually be on.
# Otherwise the configured `ROOT_URL` flips to a sub-domain
# shape that has no nginx vhost backing it → broken on the
# rebuild.
assertion = !cfg.behindGateway || (gatewayCfg.enable or false);
message = ''
services.hyperhive.forge.behindGateway = true requires
services.hyperhive.gateway.enable = true (the gateway vhost
serving forge needs the gateway container to actually be
running). Either turn the gateway on, or set
services.hyperhive.forge.behindGateway = false to keep forge
on its direct port.
'';
}
];
containers.hive-forge = {

View file

@ -69,22 +69,11 @@ in
# `docs/gateway.md`.
options.services.hyperhive.gateway = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Run hive-gateway a single nginx in front of every hyperhive
surface. On by default: the gateway hosts the matrix GUI static
dist (when `services.hyperhive.matrix.gui.enable` is true) and
proxies everything else to hive-c0re's dashboard upstream. Set
`services.hyperhive.gateway.enable = false` to bypass nginx
entirely and reach hive-c0re directly on its dashboard port
(7000 by default).
v0 is HTTP-only; TLS / public-domain shape is tracked
separately.
'';
};
# The gateway is always run alongside hyperhive (it's the single nginx
# in front of every surface and the only thing exposed to the outside);
# there is no enable flag. An operator who wants their own reverse proxy
# in front points it at the gateway's `port`. The gateway config below
# is gated on the top-level `services.hyperhive.enable`.
port = lib.mkOption {
type = lib.types.port;
@ -375,7 +364,7 @@ in
};
config = lib.mkIf cfg.enable {
config = lib.mkIf config.services.hyperhive.enable {
assertions = [
{
assertion = !cfg.localHostsEntry || hyperhiveDomain != null;

View file

@ -248,8 +248,8 @@ in
defaultText = lib.literalExpression "config.services.hyperhive.matrix.enable";
description = ''
Serve a matrix web client at `matrix.''${services.hyperhive.domain}/`.
Requires `gateway.enable` + `matrix.gatewayHost != null`
(default true / `matrix.<hive>` when hive-domain set). When
Requires `matrix.gatewayHost != null` (default `matrix.<hive>`
when hive-domain set); the gateway itself always runs. When
off, the dashboard's `M4TR1X ` tab is hidden. See
`docs/gateway.md` for the discovery flow that lets clients
auto-find the sub-domain.
@ -435,9 +435,9 @@ in
# boot failure this module fixes was an *empty* resolv.conf, a parse
# error, not a connectivity one) — so this is robustness, not a boot
# requirement. Soft `after` ordering (not `requires`) keeps the matrix
# container's lifecycle decoupled from the gateway's. `network.enable`
# asserts `gateway.enable`, so the gateway container unit always exists
# here. (Declarative `containers.<n>` → `container@<n>.service` — the
# container's lifecycle decoupled from the gateway's. The gateway
# always runs alongside hyperhive, so the gateway container unit always
# exists here. (Declarative `containers.<n>` → `container@<n>.service` — the
# nspawn template NixOS generates, confirmed from the live
# `container@hive-matrix.service` host unit.)
systemd.services."container@hive-matrix".after = lib.mkIf networkCfg.enable [

View file

@ -154,16 +154,6 @@ in
`services.hyperhive.network.enable = false` explicitly.
'';
}
{
assertion = config.services.hyperhive.gateway.enable;
message = ''
services.hyperhive.network.enable = true requires
services.hyperhive.gateway.enable = true the dnsmasq
resolver runs inside the hive-gateway container (single
front-door for both DNS and HTTP). Enable the gateway or
set `services.hyperhive.network.enable = false` explicitly.
'';
}
];
# Virtual bridge — veth pairs attach when isolateContainers flips on.
@ -196,17 +186,6 @@ 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.<domain>` which nginx (in
the gateway container) proxies to forgejo. Without the gateway
there is nothing listening on port 80 to serve that hostname.
'';
}
];
})