refactor(3202): authelia declares its own vhost and dns name
Moves the authelia vhost out of the gateway's vhosts.nix and its `address=` rule out of dnsmasq.nix, into swarm-authelia.nix. Both land inside that module's existing `cfg.enable` guard, which 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 the 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. The kit grows a fourth member, `errorPages`, because the vhost aims its 502/503/504 at the gateway's styled sso-unavailable page. Republished rather than imported per module: a service rendering its own would drift from the rest of the gateway the first time the theme changed.
This commit is contained in:
parent
d60a0585d6
commit
56ab6d26c1
6 changed files with 91 additions and 56 deletions
|
|
@ -64,11 +64,17 @@ let
|
||||||
svcCert = "${tlsDir}/swarm-services.pem";
|
svcCert = "${tlsDir}/swarm-services.pem";
|
||||||
svcKey = "${tlsDir}/swarm-services-key.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 /
|
# The vhost construction kit (listen set / per-name TLS attrs /
|
||||||
# security headers). Computed here, published as `cfg.lib` below, and
|
# security headers / error pages). Computed here, published as
|
||||||
# handed to ./vhosts.nix **as the published value** — so the tree the
|
# `cfg.lib` below, and handed to ./vhosts.nix **as the published
|
||||||
# gateway renders and the kit a service module gets are the same
|
# value** — so the tree the gateway renders and the kit a service
|
||||||
# object by construction, not by two call sites agreeing.
|
# module gets are the same object by construction, not by two call
|
||||||
|
# sites agreeing.
|
||||||
vhostLib = import ./vhost-lib.nix {
|
vhostLib = import ./vhost-lib.nix {
|
||||||
inherit
|
inherit
|
||||||
lib
|
lib
|
||||||
|
|
@ -78,6 +84,7 @@ let
|
||||||
svcCert
|
svcCert
|
||||||
svcKey
|
svcKey
|
||||||
swarmServiceDomains
|
swarmServiceDomains
|
||||||
|
errorPages
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -86,6 +93,7 @@ let
|
||||||
inherit
|
inherit
|
||||||
lib
|
lib
|
||||||
cfg
|
cfg
|
||||||
|
errorPages
|
||||||
matrixCfg
|
matrixCfg
|
||||||
autheliaCfg
|
autheliaCfg
|
||||||
uiCfg
|
uiCfg
|
||||||
|
|
@ -94,7 +102,6 @@ let
|
||||||
dashboardDist
|
dashboardDist
|
||||||
swaggerUiTheme
|
swaggerUiTheme
|
||||||
;
|
;
|
||||||
errorPages = import ./error-pages.nix { inherit pkgs; };
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
@ -373,7 +380,6 @@ in
|
||||||
cfg
|
cfg
|
||||||
networkCfg
|
networkCfg
|
||||||
matrixCfg
|
matrixCfg
|
||||||
autheliaCfg
|
|
||||||
uiCfg
|
uiCfg
|
||||||
hyperhiveDomain
|
hyperhiveDomain
|
||||||
;
|
;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
cfg, # services.hyperhive.gateway
|
cfg, # services.hyperhive.gateway
|
||||||
networkCfg,
|
networkCfg,
|
||||||
matrixCfg,
|
matrixCfg,
|
||||||
autheliaCfg,
|
|
||||||
uiCfg,
|
uiCfg,
|
||||||
hyperhiveDomain,
|
hyperhiveDomain,
|
||||||
}:
|
}:
|
||||||
|
|
@ -69,7 +68,6 @@
|
||||||
++ lib.optional (
|
++ lib.optional (
|
||||||
matrixCfg.enable && matrixCfg.gatewayHost != null
|
matrixCfg.enable && matrixCfg.gatewayHost != null
|
||||||
) "/${matrixCfg.gatewayHost}/${networkCfg.bridgeIp}"
|
) "/${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 swarm UI's name is the swarm APEX by default — a sibling of
|
||||||
# the three above, not a child of anything this resolver already
|
# the three above, not a child of anything this resolver already
|
||||||
# answers for, so the `/<hive domain>/` rule does not cover it.
|
# answers for, so the `/<hive domain>/` rule does not cover it.
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
securityHeaders = lib.mkOption {
|
||||||
type = lib.types.lines;
|
type = lib.types.lines;
|
||||||
internal = true;
|
internal = true;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
svcCert, # swarm-services leaf, for names the hive CA cannot sign
|
svcCert, # swarm-services leaf, for names the hive CA cannot sign
|
||||||
svcKey,
|
svcKey,
|
||||||
swarmServiceDomains, # which names those are (../swarm.nix derives it)
|
swarmServiceDomains, # which names those are (../swarm.nix derives it)
|
||||||
|
errorPages, # ./error-pages.nix: { notFound, unreachable, unauthorized, ssoUnavailable }
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
# nixos `services.nginx.virtualHosts.<name>` ssl attrs for a vhost
|
# nixos `services.nginx.virtualHosts.<name>` ssl attrs for a vhost
|
||||||
|
|
@ -98,4 +99,11 @@ in
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
${lib.optionalString cfg.hsts.enable ''add_header Strict-Transport-Security "${hstsDirectives}" 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,53 +36,6 @@ let
|
||||||
publicPort = cfg.httpsPort;
|
publicPort = cfg.httpsPort;
|
||||||
publicPortSuffix = if publicPort == 443 then "" else ":${toString publicPort}";
|
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
|
# Swarm UI vhost — the swarm's front page, on the swarm apex, and the
|
||||||
# FIRST `auth_request` anywhere in this gateway (everything else is
|
# FIRST `auth_request` anywhere in this gateway (everything else is
|
||||||
# `auth_basic` + htpasswd).
|
# `auth_basic` + htpasswd).
|
||||||
|
|
@ -481,7 +434,7 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// autheliaVhost
|
|
||||||
// matrixVhost
|
// matrixVhost
|
||||||
// swarmUiVhost;
|
// swarmUiVhost;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
let
|
let
|
||||||
cfg = config.services.hyperhive.swarm.authelia;
|
cfg = config.services.hyperhive.swarm.authelia;
|
||||||
hyperhiveCfg = config.services.hyperhive;
|
hyperhiveCfg = config.services.hyperhive;
|
||||||
|
gatewayCfg = hyperhiveCfg.gateway;
|
||||||
hyperhiveDomain = hyperhiveCfg.domain;
|
hyperhiveDomain = hyperhiveCfg.domain;
|
||||||
swarmDomain = hyperhiveCfg.swarm.domain;
|
swarmDomain = hyperhiveCfg.swarm.domain;
|
||||||
uiCfg = hyperhiveCfg.swarm.ui;
|
uiCfg = hyperhiveCfg.swarm.ui;
|
||||||
|
|
@ -402,6 +403,59 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf (hyperhiveCfg.enable && cfg.enable) {
|
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} = {
|
containers.${cfg.machine} = {
|
||||||
autoStart = true;
|
autoStart = true;
|
||||||
ephemeral = false;
|
ephemeral = false;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue