The dashboard vhost, the hive domain vhost and the dashboard's local name move to hive-c0re/vhosts.nix; the matrix .well-known discovery and the legacy /matrix/ redirect move to glue-matrix-hive-discovery.nix, which adds them to the hive domain vhost. Both declare through gateway.vhosts. hive-gateway/vhosts.nix keeps the catch-all only, and the gateway no longer reads deploy.matrix or c0re.servedFrontend, nor deploy.hive-controller.enable for its vhosts. Part of #3742.
213 lines
8.8 KiB
Nix
213 lines
8.8 KiB
Nix
# The hive's gateway surface, declared under `services.hyperhive.gateway.vhosts`
|
|
# like every service's: the dashboard's vhost (`swarm.dashboardDomain`), which
|
|
# carries the dashboard behind authelia, per-agent routing and the swagger UI;
|
|
# and the vhost named for the hive domain, which redirects everything to the
|
|
# dashboard's name. Matrix discovery on the hive domain is added by
|
|
# ../glue-matrix-hive-discovery.nix.
|
|
{
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
hyperhiveCfg = config.services.hyperhive;
|
|
gatewayCfg = hyperhiveCfg.gateway;
|
|
inherit (hyperhiveCfg.swarm) dashboardDomain;
|
|
autheliaDomain = hyperhiveCfg.swarm.authelia.domain;
|
|
inherit (gatewayCfg.lib) errorPages;
|
|
|
|
# Dashboard SPA dist, static-served by nginx.
|
|
dashboardDist = "${hyperhiveCfg.c0re.servedFrontend}/dashboard";
|
|
|
|
# The canonical 443 elides the port; see `docs/networking/gateway.md`
|
|
# ("Self-signed TLS").
|
|
publicPortSuffix = if gatewayCfg.httpsPort == 443 then "" else ":${toString gatewayCfg.httpsPort}";
|
|
|
|
# `/agent/` catch-all 404 + the two internal error-page targets it
|
|
# points at. Per-agent `location /agent/<name>/` blocks live in the
|
|
# runtime-generated `/var/lib/hive-gateway/conf/agents.conf` (included via
|
|
# `extraConfig` on the vhost); nginx longest-prefix-match makes a
|
|
# real `/agent/<name>/` beat this catch-all. `internal` keeps the
|
|
# error pages reachable only through nginx's error handling.
|
|
agentLocations = {
|
|
"/agent/" = {
|
|
extraConfig = ''
|
|
error_page 404 = /__hive_agent_not_found;
|
|
return 404;
|
|
'';
|
|
};
|
|
"= /__hive_agent_not_found" = {
|
|
extraConfig = ''
|
|
internal;
|
|
alias ${errorPages.notFound};
|
|
default_type text/html;
|
|
'';
|
|
};
|
|
"= /__hive_agent_unreachable" = {
|
|
extraConfig = ''
|
|
internal;
|
|
alias ${errorPages.unreachable};
|
|
default_type text/html;
|
|
'';
|
|
};
|
|
};
|
|
|
|
# Repeated verbatim by every operator-gated location (`/`, `/api/`,
|
|
# `/api/docs/`): nginx's `auth_request` does NOT inherit across sibling
|
|
# locations, so a location without it is reachable with no session.
|
|
#
|
|
# ⚠️ `auth_request` answers "is there a session", not "is this an
|
|
# operator". authelia's `operatorsOnly` rule for this name
|
|
# (../swarm-authelia.nix) is what refuses an agent's own account.
|
|
dashboardAuth = ''
|
|
auth_request /__hive_authelia;
|
|
# Captured BEFORE the error_page jump: inside the 401 handler
|
|
# `$request_uri` is the internal one.
|
|
auth_request_set $target_url $scheme://$http_host$request_uri;
|
|
error_page 401 =302 https://${autheliaDomain}/?rd=$target_url;
|
|
'';
|
|
|
|
# Dashboard: nginx static-serves the dist, c0re is API-only. Routing
|
|
# is by PATH, never content-type. c0re serves exactly two prefixes —
|
|
# `/api/` (all dashboard data + actions + the SSE streams) and
|
|
# `/health/` (liveness + readiness) — so those proxy to c0re and
|
|
# everything else serves the dist with an SPA fallback to index.html.
|
|
# Path routing is deterministic where an Accept-header split would make
|
|
# the SAME url behave differently by content-type (e.g. `/api/state`
|
|
# fetched with `Accept: text/html` wrongly getting index.html). A new
|
|
# top-level c0re route prefix (beyond /api + /health) needs
|
|
# a matching location added here.
|
|
dashboardProxyLocation = {
|
|
"/" = {
|
|
root = dashboardDist;
|
|
extraConfig = ''
|
|
try_files $uri /index.html;
|
|
${dashboardAuth}
|
|
'';
|
|
};
|
|
"/api/" = {
|
|
proxyPass = "http://${gatewayCfg.upstreamHost}:${toString gatewayCfg.upstreamPort}";
|
|
proxyWebsockets = true;
|
|
extraConfig = ''
|
|
# off + 1d keep the SSE streams (/api/dashboard/stream,
|
|
# /api/build-logs/id/{id}/stream) live.
|
|
proxy_buffering off;
|
|
proxy_read_timeout 1d;
|
|
${dashboardAuth}
|
|
'';
|
|
};
|
|
"/health/" = {
|
|
# No dashboardAuth here: an external uptime monitor can't do an
|
|
# interactive login. The endpoints themselves are scoped to
|
|
# status + warning kind/message (see hive-c0re/src/dashboard/
|
|
# health.rs) — no tokens, no agent detail — so exposing them
|
|
# unauthenticated isn't a new secret surface.
|
|
proxyPass = "http://${gatewayCfg.upstreamHost}:${toString gatewayCfg.upstreamPort}";
|
|
};
|
|
};
|
|
|
|
# Swagger UI: nginx hosts the FULL themed dist (`gateway.swaggerUiTheme` —
|
|
# vendored Swagger UI + our overlay, see nix/packages/swagger-ui-
|
|
# dist.nix + swagger-ui-theme.nix) straight from the store, with NO
|
|
# fallback to hive-c0re at all: core hosts only the JSON. Only
|
|
# `/api/openapi.json` (the live-generated spec `index.html` fetches;
|
|
# not under this prefix) proxies to c0re via "/api/" above — that's
|
|
# the one thing that has to stay dynamic.
|
|
#
|
|
# Prefix location, not exact-match: wins over "/api/" on plain
|
|
# prefix length (no ordering/`=` needed), and covers every file in
|
|
# the tree (bundle.js, maps, favicons, …), not just our 2 override
|
|
# files — hive-c0re serves none of this as a fallback.
|
|
# `= /api/docs` (no trailing slash) redirects to `/api/docs/`: nginx
|
|
# owns that redirect because hive-c0re has no route for it, and the
|
|
# dashboard's own `/api/docs` links (no trailing slash) would 404
|
|
# without it.
|
|
swaggerUiLocations = {
|
|
"= /api/docs" = {
|
|
extraConfig = ''
|
|
return 301 /api/docs/;
|
|
'';
|
|
};
|
|
"/api/docs/" = {
|
|
alias = "${gatewayCfg.swaggerUiTheme}/";
|
|
extraConfig = ''
|
|
index index.html;
|
|
${dashboardAuth}
|
|
'';
|
|
};
|
|
};
|
|
|
|
# The subrequest `dashboardAuth` makes, the same as the swarm UI's
|
|
# (../swarm-ui.nix), which documents the header set.
|
|
autheliaLocation = {
|
|
"= /__hive_authelia" = {
|
|
proxyPass = "https://${autheliaDomain}/api/authz/auth-request";
|
|
# nixpkgs appends its OWN `Host $host` after extraConfig, which would
|
|
# override verifiedProxyTo's — see the comment on verifiedProxyTo in
|
|
# ../hive-gateway/vhost-lib.nix.
|
|
recommendedProxySettings = false;
|
|
extraConfig = ''
|
|
internal;
|
|
${gatewayCfg.lib.verifiedProxyTo autheliaDomain}
|
|
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;
|
|
'';
|
|
};
|
|
};
|
|
in
|
|
{
|
|
# Gated on the gateway too: a hive may run with the gateway off, and a
|
|
# vhost declared then fails the eval (../hive-gateway/declared-vhosts.nix).
|
|
config =
|
|
lib.mkIf
|
|
(hyperhiveCfg.deploy.hive-controller.enable && gatewayCfg.enable && dashboardDomain != null)
|
|
{
|
|
# The dashboard's name, for agents on the bridge and (with
|
|
# `localHostsEntry`) the host's own browser.
|
|
services.hyperhive.gateway.localNames = [ dashboardDomain ];
|
|
|
|
services.hyperhive.gateway.vhosts = {
|
|
# The dashboard, reachable by its own NAME under the swarm domain,
|
|
# where authelia's session cookie reaches. The hive leaf carrying
|
|
# this name exists only where a hive runs.
|
|
#
|
|
# The agent routes live here and not on the hive domain because the
|
|
# agent page calls the dashboard's `/api/` on its own origin
|
|
# (frontend/packages/agent/src/lib/dashboardBase.ts), and the
|
|
# dashboard links to them by path. They carry no `auth_request`.
|
|
#
|
|
# `forceSSL` for the reason ../swarm-ui.nix gives: authelia answers
|
|
# the subrequest for an `http://` target with 400, which
|
|
# `auth_request` cannot read.
|
|
${dashboardDomain} = {
|
|
forceSSL = true;
|
|
locations = agentLocations // dashboardProxyLocation // swaggerUiLocations // autheliaLocation;
|
|
# Per-agent location blocks, generated at runtime by
|
|
# hive-c0re and written to /var/lib/hive-gateway/conf/agents.conf
|
|
# on the host — the same machine nginx runs on. nginx parses
|
|
# `include` at config-load time so a reload (triggered by c0re
|
|
# after each agents.conf write) picks up new or removed
|
|
# agents without a nixos-rebuild. nginx's longest-prefix-
|
|
# match rule ensures `/agent/<name>/` from this file beats
|
|
# the `/agent/` catch-all above.
|
|
extraConfig = ''
|
|
include /var/lib/hive-gateway/conf/agents.conf;
|
|
'';
|
|
};
|
|
|
|
# The hive domain's own vhost: a redirect of every path to the
|
|
# dashboard's name, so a bookmarked dashboard or agent link still
|
|
# lands.
|
|
${hyperhiveCfg.domain} = {
|
|
locations."/".extraConfig = ''
|
|
return 301 https://${dashboardDomain}${publicPortSuffix}$request_uri;
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
}
|