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:
parent
6caf177416
commit
030eef0948
4 changed files with 134 additions and 143 deletions
|
|
@ -22,9 +22,6 @@ let
|
||||||
# same list rather than each deciding what "a swarm service" means.
|
# same list rather than each deciding what "a swarm service" means.
|
||||||
swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains;
|
swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains;
|
||||||
matrixCfg = config.services.hyperhive.swarm.matrix;
|
matrixCfg = config.services.hyperhive.swarm.matrix;
|
||||||
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
|
||||||
uiCfg = config.services.hyperhive.swarm.ui;
|
|
||||||
controllerCfg = config.services.hyperhive.swarm.controller;
|
|
||||||
networkCfg = config.services.hyperhive.network;
|
networkCfg = config.services.hyperhive.network;
|
||||||
|
|
||||||
# Dashboard SPA dist, static-served by nginx.
|
# Dashboard SPA dist, static-served by nginx.
|
||||||
|
|
@ -95,9 +92,6 @@ let
|
||||||
cfg
|
cfg
|
||||||
errorPages
|
errorPages
|
||||||
matrixCfg
|
matrixCfg
|
||||||
autheliaCfg
|
|
||||||
uiCfg
|
|
||||||
controllerCfg
|
|
||||||
hyperhiveDomain
|
hyperhiveDomain
|
||||||
dashboardDist
|
dashboardDist
|
||||||
swaggerUiTheme
|
swaggerUiTheme
|
||||||
|
|
@ -379,7 +373,6 @@ in
|
||||||
lib
|
lib
|
||||||
cfg
|
cfg
|
||||||
networkCfg
|
networkCfg
|
||||||
uiCfg
|
|
||||||
hyperhiveDomain
|
hyperhiveDomain
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
|
@ -393,19 +386,18 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
# `/etc/hosts` entries for local dev — bare hive domain + any
|
# `/etc/hosts` entries for local dev — the bare hive domain plus
|
||||||
# sub-domain modules that are on. `lib.unique` dedupes if any
|
# every name a service module contributed. See `docs/gateway.md`
|
||||||
# sub-domain happens to equal another. See `docs/gateway.md`
|
|
||||||
# ("Local dev").
|
# ("Local dev").
|
||||||
|
#
|
||||||
|
# This used to restate the per-service list a THIRD time (after the
|
||||||
|
# vhosts and the dnsmasq records), with its own copy of each
|
||||||
|
# service's guard. It is the same question — "which names does this
|
||||||
|
# host answer for" — so it reads the same answer; a service added
|
||||||
|
# later lands here with no edit, and cannot land here with a
|
||||||
|
# different condition than it used for DNS.
|
||||||
networking.hosts = lib.mkIf cfg.localHostsEntry {
|
networking.hosts = lib.mkIf cfg.localHostsEntry {
|
||||||
"127.0.0.1" = lib.unique (
|
"127.0.0.1" = lib.unique ([ hyperhiveDomain ] ++ cfg.localNames);
|
||||||
[ hyperhiveDomain ]
|
|
||||||
++ lib.optional (config.services.hyperhive.swarm.forge.behindGateway or false
|
|
||||||
) config.services.hyperhive.swarm.forge.domain
|
|
||||||
++ lib.optional (matrixCfg.enable && matrixCfg.gatewayHost != null) matrixCfg.gatewayHost
|
|
||||||
++ lib.optional autheliaCfg.enable autheliaCfg.domain
|
|
||||||
++ lib.optional uiCfg.enable uiCfg.domain
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
lib,
|
lib,
|
||||||
cfg, # services.hyperhive.gateway
|
cfg, # services.hyperhive.gateway
|
||||||
networkCfg,
|
networkCfg,
|
||||||
uiCfg,
|
|
||||||
hyperhiveDomain,
|
hyperhiveDomain,
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
|
|
@ -56,15 +55,6 @@
|
||||||
address = [
|
address = [
|
||||||
"/${hyperhiveDomain}/${networkCfg.bridgeIp}"
|
"/${hyperhiveDomain}/${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 `/<hive domain>/` rule does not cover it.
|
|
||||||
#
|
|
||||||
# Published to agents deliberately (mara: publishing it is fine).
|
|
||||||
# Reachability is not the access control here: the vhost's
|
|
||||||
# `auth_request` + authelia's `group:operators` rule are, and an
|
|
||||||
# agent that resolves the name still cannot open the page.
|
|
||||||
++ lib.optional uiCfg.enable "/${uiCfg.domain}/${networkCfg.bridgeIp}"
|
|
||||||
# Names contributed by the modules that own them
|
# Names contributed by the modules that own them
|
||||||
# (`gateway.localNames`). Same address as everything above — the
|
# (`gateway.localNames`). Same address as everything above — the
|
||||||
# bridge IP is the gateway's answer for anything it fronts, and a
|
# bridge IP is the gateway's answer for anything it fronts, and a
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@
|
||||||
lib,
|
lib,
|
||||||
cfg, # services.hyperhive.gateway
|
cfg, # services.hyperhive.gateway
|
||||||
matrixCfg,
|
matrixCfg,
|
||||||
autheliaCfg, # services.hyperhive.swarm.authelia
|
|
||||||
uiCfg, # services.hyperhive.swarm.ui
|
|
||||||
controllerCfg, # services.hyperhive.swarm.controller
|
|
||||||
hyperhiveDomain,
|
hyperhiveDomain,
|
||||||
dashboardDist,
|
dashboardDist,
|
||||||
swaggerUiTheme, # nix/packages/swagger-ui-theme.nix: has index.html + hyperhive-theme.css
|
swaggerUiTheme, # nix/packages/swagger-ui-theme.nix: has index.html + hyperhive-theme.css
|
||||||
|
|
@ -36,115 +33,6 @@ let
|
||||||
publicPort = cfg.httpsPort;
|
publicPort = cfg.httpsPort;
|
||||||
publicPortSuffix = if publicPort == 443 then "" else ":${toString publicPort}";
|
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
|
# `<hive>/matrix/*` → 301 → `matrix.<hive>/$1` (legacy deep-link
|
||||||
# shim during the fluffychat sub-domain move). See `docs/gateway.md`.
|
# shim during the fluffychat sub-domain move). See `docs/gateway.md`.
|
||||||
matrixRedirectLocations =
|
matrixRedirectLocations =
|
||||||
|
|
@ -367,7 +255,5 @@ in
|
||||||
include /var/lib/hive-gateway/conf/agents.conf;
|
include /var/lib/hive-gateway/conf/agents.conf;
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
// swarmUiVhost;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,24 @@
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.services.hyperhive.swarm.ui;
|
cfg = config.services.hyperhive.swarm.ui;
|
||||||
|
gatewayCfg = config.services.hyperhive.gateway;
|
||||||
|
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
||||||
|
controllerCfg = config.services.hyperhive.swarm.controller;
|
||||||
|
|
||||||
|
# Repeated verbatim by every location that should be operator-gated
|
||||||
|
# (`/`, `/api/`, `/api/docs/`) rather than set once on the server:
|
||||||
|
# nginx's `auth_request` does NOT inherit across sibling locations, so
|
||||||
|
# setting it only on the page would leave this UI's own API and API
|
||||||
|
# docs reachable without a session.
|
||||||
|
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;
|
||||||
|
'';
|
||||||
swarmCfg = config.services.hyperhive.swarm;
|
swarmCfg = config.services.hyperhive.swarm;
|
||||||
hiveDomain = config.services.hyperhive.domain;
|
hiveDomain = config.services.hyperhive.domain;
|
||||||
in
|
in
|
||||||
|
|
@ -93,5 +111,110 @@ in
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# The swarm UI's own gateway surface. Published to agents on the
|
||||||
|
# bridge deliberately: reachability is not the access control here —
|
||||||
|
# the `auth_request` below and authelia's `group:operators` rule
|
||||||
|
# are, and an agent that resolves the name still cannot open the
|
||||||
|
# page.
|
||||||
|
#
|
||||||
|
# The apex is a SIBLING of `forge.<swarm>` / `chat.<swarm>`, not a
|
||||||
|
# child of anything the resolver already answers for, so the
|
||||||
|
# `/<hive domain>/` rule does not cover it and this record is what
|
||||||
|
# makes the name resolve at all.
|
||||||
|
services.hyperhive.gateway.localNames = [ cfg.domain ];
|
||||||
|
|
||||||
|
# The swarm's front page, 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
|
||||||
|
# have 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 why the
|
||||||
|
# redirect target and the header set below come from a measured
|
||||||
|
# source rather than 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. The shared listen set 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.
|
||||||
|
services.nginx.virtualHosts."${cfg.domain}" =
|
||||||
|
(builtins.removeAttrs (gatewayCfg.lib.tlsFor cfg.domain) [ "addSSL" ])
|
||||||
|
// {
|
||||||
|
forceSSL = true;
|
||||||
|
listen = gatewayCfg.lib.listen;
|
||||||
|
extraConfig = gatewayCfg.lib.securityHeaders;
|
||||||
|
locations = {
|
||||||
|
"/" = {
|
||||||
|
root = "${cfg.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`.
|
||||||
|
"= /api/docs" = {
|
||||||
|
extraConfig = ''
|
||||||
|
return 301 /api/docs/;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"/api/docs/" = {
|
||||||
|
alias = "${gatewayCfg.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;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue