refactor(3202): the swarm UI declares its own vhost and dns name

Last of the four. The vhost, its `auth_request` block and the swarm
apex's dns record move into swarm-ui.nix; vhosts.nix drops `uiCfg`,
`controllerCfg` and `autheliaCfg` and is now 259 lines of hive surface
with no swarm service in it.

Also collapses a THIRD copy of the per-service list. `networking.hosts`
restated every service's name with its own copy of that service's guard,
after the vhosts and the dnsmasq records had each done the same. It asks
the same question — which names does this host answer for — so it now
reads the same answer: a service added later lands in /etc/hosts with no
edit, and cannot land there under a different condition than it used for
DNS.

The `forceSSL`-not-`addSSL` comment travels intact: it records that
authelia answers an http auth subrequest with 400 and nginx's
auth_request only understands 2xx/401/403, so the scheme is load-bearing
for this vhost and no other.
This commit is contained in:
atlas 2026-08-13 15:24:21 +02:00
commit 030eef0948
4 changed files with 134 additions and 143 deletions

View file

@ -8,9 +8,6 @@
lib,
cfg, # services.hyperhive.gateway
matrixCfg,
autheliaCfg, # services.hyperhive.swarm.authelia
uiCfg, # services.hyperhive.swarm.ui
controllerCfg, # services.hyperhive.swarm.controller
hyperhiveDomain,
dashboardDist,
swaggerUiTheme, # nix/packages/swagger-ui-theme.nix: has index.html + hyperhive-theme.css
@ -36,115 +33,6 @@ let
publicPort = cfg.httpsPort;
publicPortSuffix = if publicPort == 443 then "" else ":${toString publicPort}";
# Swarm UI vhost — the swarm's front page, on the swarm apex, and the
# FIRST `auth_request` anywhere in this gateway (everything else is
# `auth_basic` + htpasswd).
#
# ⚠️ `auth_request` answers "is there a session", not "is this an
# operator". The operator-only part is authelia's `access_control`
# rule (../swarm-authelia.nix) requiring `group:operators` — agents
# are getting authelia accounts of their own, and without that rule a
# session alone would open this page.
#
# ⚠️ Failure mode here is LOCKED OUT, not unprotected: a subrequest
# that wrongly denies takes the whole UI away. That is the reason the
# redirect target and the header set below are copied from a measured
# source rather than from an example.
# ⚠️ `forceSSL`, not `addSSL` like every other vhost — not a hardening
# preference, the only way this page works at all. authelia answers the
# auth subrequest for an `http://` target with **400**, and nginx's
# `auth_request` only understands 2xx/401/403, so a plain-http visit
# dies as "auth request unexpected status: 400" with no hint a login
# exists. `vhostListen` binds :80, so without this the door is open on
# a port the lock cannot work on. Serving forge or matrix over http is
# merely insecure rather than broken, so they keep `addSSL` and the
# asymmetry stays local to the vhost whose correctness depends on the
# scheme. `removeAttrs` because nixos asserts on a vhost declaring both.
# Shared with every swarm-UI-vhost location below (`/`, `/api/`,
# `/api/docs/`) — auth_request does not inherit across sibling
# locations, so each one that should be operator-gated repeats this
# verbatim rather than only the page itself being protected while its
# own API and API docs are reachable unauthenticated.
swarmAuthRequest = ''
auth_request /__hive_authelia;
# Captured BEFORE the error_page jump: inside the 401 handler
# `$request_uri` is the internal one, so building the return
# link there sends the operator back to the auth subrequest
# instead of the page they asked for.
auth_request_set $target_url $scheme://$http_host$request_uri;
error_page 401 =302 https://${autheliaCfg.domain}/?rd=$target_url;
'';
swarmUiVhost = lib.optionalAttrs uiCfg.enable {
"${uiCfg.domain}" = (builtins.removeAttrs (vhostTlsFor uiCfg.domain) [ "addSSL" ]) // {
forceSSL = true;
listen = vhostListen;
extraConfig = securityHeaders;
locations = {
"/" = {
root = "${uiCfg.package}";
extraConfig = ''
${swarmAuthRequest}
# SPA: any path the bundle routes client-side is served the
# entry document rather than a 404 from the filesystem.
try_files $uri /index.html;
'';
};
# swarm-controller's whole HTTP surface, including the live
# `/api/openapi.json` spec — proxied untouched (no URI segment
# after the socket path, same "pass the request through as-is"
# shape as the per-hive dashboard's own `/api/` proxy) so the
# path swarm-controller registered a route at is the path
# nginx forwards, no prefix-stripping to keep in sync by hand.
"/api/" = {
proxyPass = "http://unix:${controllerCfg.socketPath}:";
extraConfig = swarmAuthRequest;
};
# Swagger UI: same "nginx hosts the themed dist straight from
# the store, only /api/openapi.json is dynamic" shape as the
# per-hive gateway's `swaggerUiLocations` — see that block's
# comment for why core-equivalent (here, swarm-controller)
# does not also mount its own copy.
"= /api/docs" = {
extraConfig = ''
return 301 /api/docs/;
'';
};
"/api/docs/" = {
alias = "${swaggerUiTheme}/";
extraConfig = ''
index index.html;
${swarmAuthRequest}
'';
};
# The subrequest itself. `auth-request` is the implementation
# name authelia exposes under `/api/authz/`; `/api/verify` is the
# LEGACY path every older example shows.
#
# Header set measured against the pinned binary (4.39.20), not
# copied: `X-Original-URL` and `X-Original-Method` are present as
# literals and are what this implementation reads —
# `X-Forwarded-Uri` does not appear in it at all, so sending it
# would look like configuration and be dead weight.
"= /__hive_authelia" = {
proxyPass = "http://127.0.0.1:${toString autheliaCfg.port}/api/authz/auth-request";
extraConfig = ''
internal;
# A subrequest carries no body, and forwarding one here makes
# authelia read a payload it will never use.
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
'';
};
};
};
};
# `<hive>/matrix/*` → 301 → `matrix.<hive>/$1` (legacy deep-link
# shim during the fluffychat sub-domain move). See `docs/gateway.md`.
matrixRedirectLocations =
@ -367,7 +255,5 @@ in
include /var/lib/hive-gateway/conf/agents.conf;
'';
};
}
// swarmUiVhost;
};
}