From 0dc2e6b64f5df47dc423e14d6494e36cbf1d874e Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 12 Aug 2026 17:40:24 +0200 Subject: [PATCH] feat(3167): the swarm UI vhost, behind an authelia subrequest Serves the static bundle on the swarm apex and gates it with auth_request - the first one in this gateway, everything else being auth_basic + htpasswd. Header set measured against the pinned authelia (4.39.20) rather than copied from an example: X-Original-URL and X-Original-Method are present as literals and are what the auth-request implementation reads, while X-Forwarded-Uri does not appear in that binary at all - sending it would look like configuration and be dead weight. The endpoint is /api/authz/auth-request; /api/verify is the legacy path older examples show. auth_request_set captures the return URL BEFORE the error_page jump: in the 401 handler $request_uri is the internal one, so building the link there sends the operator back to the auth subrequest rather than the page they asked for. Authorisation is the access_control rule from the previous commit, not this subrequest: auth_request answers 'is there a session'. --- nix/host-modules/hive-gateway/default.nix | 2 + nix/host-modules/hive-gateway/vhosts.nix | 66 ++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/nix/host-modules/hive-gateway/default.nix b/nix/host-modules/hive-gateway/default.nix index f8122f13..0addddfe 100644 --- a/nix/host-modules/hive-gateway/default.nix +++ b/nix/host-modules/hive-gateway/default.nix @@ -23,6 +23,7 @@ let swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains; matrixCfg = config.services.hyperhive.swarm.matrix; autheliaCfg = config.services.hyperhive.swarm.authelia; + uiCfg = config.services.hyperhive.swarm.ui; forgeCfg = config.services.hyperhive.swarm.forge; networkCfg = config.services.hyperhive.network; @@ -70,6 +71,7 @@ let forgeCfg matrixCfg autheliaCfg + uiCfg hyperhiveDomain dashboardDist swaggerUiTheme diff --git a/nix/host-modules/hive-gateway/vhosts.nix b/nix/host-modules/hive-gateway/vhosts.nix index 28acb246..cf4ecb4f 100644 --- a/nix/host-modules/hive-gateway/vhosts.nix +++ b/nix/host-modules/hive-gateway/vhosts.nix @@ -10,6 +10,7 @@ forgeCfg, matrixCfg, autheliaCfg, # services.hyperhive.swarm.authelia + uiCfg, # services.hyperhive.swarm.ui hyperhiveDomain, dashboardDist, swaggerUiTheme, # nix/packages/swagger-ui-theme.nix: has index.html + hyperhive-theme.css @@ -171,6 +172,68 @@ let }; }; + # 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. + swarmUiVhost = lib.optionalAttrs uiCfg.enable { + "${uiCfg.domain}" = (vhostTlsFor uiCfg.domain) // { + listen = vhostListen; + extraConfig = securityHeaders; + locations = { + "/" = { + root = "${uiCfg.package}"; + extraConfig = '' + 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; + # 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; + ''; + }; + # 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; + ''; + }; + }; + }; + }; + # Matrix sub-domain vhost. `server_name = matrixCfg.gatewayHost`. # `/_matrix/*` → tuwunel (CORS *, 50M body cap, 1h long-poll # timeout). `/` serves fluffychat or 404 if GUI off. nginx @@ -458,5 +521,6 @@ in } // forgeVhost // autheliaVhost - // matrixVhost; + // matrixVhost + // swarmUiVhost; }