diff --git a/nix/host-modules/hive-gateway/default.nix b/nix/host-modules/hive-gateway/default.nix index a2cdc95e..0b9deea5 100644 --- a/nix/host-modules/hive-gateway/default.nix +++ b/nix/host-modules/hive-gateway/default.nix @@ -64,11 +64,17 @@ let svcCert = "${tlsDir}/swarm-services.pem"; svcKey = "${tlsDir}/swarm-services-key.pem"; + # Styled static error pages. Built once here and reached two ways: + # directly by ./vhosts.nix, and via the published kit by any service + # module that aims an `error_page` at one. + errorPages = import ./error-pages.nix { inherit pkgs; }; + # The vhost construction kit (listen set / per-name TLS attrs / - # security headers). Computed here, published as `cfg.lib` below, and - # handed to ./vhosts.nix **as the published value** — so the tree the - # gateway renders and the kit a service module gets are the same - # object by construction, not by two call sites agreeing. + # security headers / error pages). Computed here, published as + # `cfg.lib` below, and handed to ./vhosts.nix **as the published + # value** — so the tree the gateway renders and the kit a service + # module gets are the same object by construction, not by two call + # sites agreeing. vhostLib = import ./vhost-lib.nix { inherit lib @@ -78,6 +84,7 @@ let svcCert svcKey swarmServiceDomains + errorPages ; }; @@ -86,6 +93,7 @@ let inherit lib cfg + errorPages matrixCfg autheliaCfg uiCfg @@ -94,7 +102,6 @@ let dashboardDist swaggerUiTheme ; - errorPages = import ./error-pages.nix { inherit pkgs; }; }; in { @@ -373,7 +380,6 @@ in cfg networkCfg matrixCfg - autheliaCfg uiCfg hyperhiveDomain ; diff --git a/nix/host-modules/hive-gateway/dnsmasq.nix b/nix/host-modules/hive-gateway/dnsmasq.nix index 89e6f952..80ce77c8 100644 --- a/nix/host-modules/hive-gateway/dnsmasq.nix +++ b/nix/host-modules/hive-gateway/dnsmasq.nix @@ -10,7 +10,6 @@ cfg, # services.hyperhive.gateway networkCfg, matrixCfg, - autheliaCfg, uiCfg, hyperhiveDomain, }: @@ -69,7 +68,6 @@ ++ lib.optional ( matrixCfg.enable && matrixCfg.gatewayHost != null ) "/${matrixCfg.gatewayHost}/${networkCfg.bridgeIp}" - ++ lib.optional autheliaCfg.enable "/${autheliaCfg.domain}/${networkCfg.bridgeIp}" # The swarm UI's name is the swarm APEX by default — a sibling of # the three above, not a child of anything this resolver already # answers for, so the `//` rule does not cover it. diff --git a/nix/host-modules/hive-gateway/options.nix b/nix/host-modules/hive-gateway/options.nix index 48de8a1f..74bef4da 100644 --- a/nix/host-modules/hive-gateway/options.nix +++ b/nix/host-modules/hive-gateway/options.nix @@ -150,6 +150,22 @@ in ''; }; + errorPages = lib.mkOption { + type = lib.types.attrsOf lib.types.path; + internal = true; + readOnly = true; + description = '' + Read-only: the gateway's styled static error pages, by name + (`notFound`, `unreachable`, `unauthorized`, `ssoUnavailable`). + + Published so a service module can aim an `error_page` at one + instead of rendering its own — a service that built its own + would drift from the rest of the gateway the first time the + theme changed, and the operator would meet two different + error styles on one hive. + ''; + }; + securityHeaders = lib.mkOption { type = lib.types.lines; internal = true; diff --git a/nix/host-modules/hive-gateway/vhost-lib.nix b/nix/host-modules/hive-gateway/vhost-lib.nix index d60e52d2..060c14c1 100644 --- a/nix/host-modules/hive-gateway/vhost-lib.nix +++ b/nix/host-modules/hive-gateway/vhost-lib.nix @@ -22,6 +22,7 @@ svcCert, # swarm-services leaf, for names the hive CA cannot sign svcKey, swarmServiceDomains, # which names those are (../swarm.nix derives it) + errorPages, # ./error-pages.nix: { notFound, unreachable, unauthorized, ssoUnavailable } }: let # nixos `services.nginx.virtualHosts.` ssl attrs for a vhost @@ -98,4 +99,11 @@ in add_header Referrer-Policy "strict-origin-when-cross-origin" always; ${lib.optionalString cfg.hsts.enable ''add_header Strict-Transport-Security "${hstsDirectives}" always;''} ''; + + # The gateway's styled error pages, re-exported so a service module + # can point an `error_page` at one. Republished rather than imported + # per module for the same reason as everything else in this kit: these + # carry the hive's branding, and a service rendering its own would + # drift from the rest of the gateway the first time the theme changes. + inherit errorPages; } diff --git a/nix/host-modules/hive-gateway/vhosts.nix b/nix/host-modules/hive-gateway/vhosts.nix index e2a5a1e9..d5d15a4d 100644 --- a/nix/host-modules/hive-gateway/vhosts.nix +++ b/nix/host-modules/hive-gateway/vhosts.nix @@ -36,53 +36,6 @@ let publicPort = cfg.httpsPort; publicPortSuffix = if publicPort == 443 then "" else ":${toString publicPort}"; - # Authelia sub-domain vhost. `server_name = authelia.domain`, all of - # `/` → authelia. Empty attrset unless THIS host runs the container: - # every hive knows the swarm's `authelia.url`, but only the one - # serving it may claim the name — a client hive declaring this vhost - # would answer for a service it does not run. - # - # ⚠️ The server name must be exactly `autheliaCfg.domain`, not a - # near-miss: authelia validates `authelia_url ⊂ session cookie domain` - # at STARTUP, so a mismatch is a container that refuses to boot rather - # than a login that misbehaves. - # - # ⚠️ And deliberately NO `dashboardAuth` here. That block is the - # gateway's `auth_basic`; applying it to the SSO provider would put - # the login page behind the login mechanism it exists to replace. - autheliaVhost = lib.optionalAttrs autheliaCfg.enable { - "${autheliaCfg.domain}" = (vhostTlsFor autheliaCfg.domain) // { - listen = vhostListen; - extraConfig = securityHeaders; - locations."/" = { - proxyPass = "http://127.0.0.1:${toString autheliaCfg.port}/"; - proxyWebsockets = true; - extraConfig = '' - proxy_buffering off; - # authelia decides by the ORIGINAL request, not by the hop it - # sees — the login redirect and the session cookie's domain - # both derive from these. Without them every request looks - # like it arrived at 127.0.0.1 over plain http. - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Uri $request_uri; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - # A dead upstream here means "not bootstrapped" far more often - # than "misconfigured proxy", and a bare 502 says the opposite. - proxy_intercept_errors on; - error_page 502 503 504 = /__hive_sso_unavailable; - ''; - }; - locations."= /__hive_sso_unavailable" = { - extraConfig = '' - internal; - alias ${errorPages.ssoUnavailable}; - default_type text/html; - ''; - }; - }; - }; - # 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). @@ -481,7 +434,7 @@ in ''; }; } - // autheliaVhost + // matrixVhost // swarmUiVhost; } diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index 30d39fd5..37e6a511 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -35,6 +35,7 @@ let cfg = config.services.hyperhive.swarm.authelia; hyperhiveCfg = config.services.hyperhive; + gatewayCfg = hyperhiveCfg.gateway; hyperhiveDomain = hyperhiveCfg.domain; swarmDomain = hyperhiveCfg.swarm.domain; uiCfg = hyperhiveCfg.swarm.ui; @@ -402,6 +403,59 @@ in }; config = lib.mkIf (hyperhiveCfg.enable && cfg.enable) { + # Authelia's own gateway surface: the vhost that fronts it and the + # name the hive resolver answers for. Both live here rather than in + # the gateway, and both are inside `cfg.enable` — that guard is the + # load-bearing part. + # + # ⚠️ Every hive in a swarm knows `authelia.url`, but only the host + # that RUNS the container may claim the name. A client hive + # declaring this vhost would answer for a service it does not run, + # and publishing the DNS record would point every agent on its + # bridge at that wrong answer. + services.hyperhive.gateway.localNames = [ cfg.domain ]; + + # `server_name = authelia.domain`, all of `/` → authelia. + # + # ⚠️ The server name must be exactly `cfg.domain`, not a near-miss: + # authelia validates `authelia_url ⊂ session cookie domain` at + # STARTUP, so a mismatch is a container that refuses to boot rather + # than a login that misbehaves. + # + # ⚠️ And deliberately NO `dashboardAuth` here. That block is the + # gateway's `auth_basic`; applying it to the SSO provider would put + # the login page behind the login mechanism it exists to replace. + services.nginx.virtualHosts."${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // { + listen = gatewayCfg.lib.listen; + extraConfig = gatewayCfg.lib.securityHeaders; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString cfg.port}/"; + proxyWebsockets = true; + extraConfig = '' + proxy_buffering off; + # authelia decides by the ORIGINAL request, not by the hop it + # sees — the login redirect and the session cookie's domain + # both derive from these. Without them every request looks + # like it arrived at 127.0.0.1 over plain http. + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Uri $request_uri; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # A dead upstream here means "not bootstrapped" far more often + # than "misconfigured proxy", and a bare 502 says the opposite. + proxy_intercept_errors on; + error_page 502 503 504 = /__hive_sso_unavailable; + ''; + }; + locations."= /__hive_sso_unavailable" = { + extraConfig = '' + internal; + alias ${gatewayCfg.lib.errorPages.ssoUnavailable}; + default_type text/html; + ''; + }; + }; + containers.${cfg.machine} = { autoStart = true; ephemeral = false;