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

View file

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

View file

@ -112,8 +112,8 @@ in
behindGateway = lib.mkOption { behindGateway = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = gatewayCfg.enable or false; default = config.services.hyperhive.enable;
defaultText = lib.literalExpression "config.services.hyperhive.gateway.enable"; defaultText = lib.literalExpression "config.services.hyperhive.enable";
description = '' description = ''
Serve forgejo through the hive-gateway nginx as a sub-domain Serve forgejo through the hive-gateway nginx as a sub-domain
vhost (`server_name = cfg.domain`) instead of directly on vhost (`server_name = cfg.domain`) instead of directly on
@ -127,9 +127,9 @@ in
- `gateway.localHostsEntry = true` extends `/etc/hosts` to - `gateway.localHostsEntry = true` extends `/etc/hosts` to
include `cfg.domain 127.0.0.1` for local dev. include `cfg.domain 127.0.0.1` for local dev.
Defaults to `services.hyperhive.gateway.enable` flipping Defaults to `services.hyperhive.enable` (the gateway always runs
the gateway on/off auto-routes forge through it. Set `false` alongside hyperhive, so forge auto-routes through it). Set `false`
explicitly to keep forge on the direct port even when the explicitly to keep forge on the direct port even though the
gateway is running (e.g. an external git client that doesn't gateway is running (e.g. an external git client that doesn't
traverse the gateway). traverse the gateway).
@ -211,21 +211,6 @@ in
or "git.internal". 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 = { containers.hive-forge = {

View file

@ -69,22 +69,11 @@ in
# `docs/gateway.md`. # `docs/gateway.md`.
options.services.hyperhive.gateway = { options.services.hyperhive.gateway = {
enable = lib.mkOption { # The gateway is always run alongside hyperhive (it's the single nginx
type = lib.types.bool; # in front of every surface and the only thing exposed to the outside);
default = true; # there is no enable flag. An operator who wants their own reverse proxy
description = '' # in front points it at the gateway's `port`. The gateway config below
Run hive-gateway a single nginx in front of every hyperhive # is gated on the top-level `services.hyperhive.enable`.
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.
'';
};
port = lib.mkOption { port = lib.mkOption {
type = lib.types.port; type = lib.types.port;
@ -375,7 +364,7 @@ in
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf config.services.hyperhive.enable {
assertions = [ assertions = [
{ {
assertion = !cfg.localHostsEntry || hyperhiveDomain != null; assertion = !cfg.localHostsEntry || hyperhiveDomain != null;

View file

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

View file

@ -154,16 +154,6 @@ in
`services.hyperhive.network.enable = false` explicitly. `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. # Virtual bridge — veth pairs attach when isolateContainers flips on.
@ -196,17 +186,6 @@ in
resolver must be running before isolation is flipped on). 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.
'';
}
]; ];
}) })