move the hive's vhosts and matrix discovery out of the gateway
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.
This commit is contained in:
parent
2489830772
commit
b4c5954491
17 changed files with 391 additions and 378 deletions
|
|
@ -1,311 +1,40 @@
|
|||
# The gateway's own vhosts, as `services.hyperhive.gateway.vhosts`
|
||||
# declarations (rendered by ./declared-vhosts.nix): the `_` default server,
|
||||
# which answers unmatched `Host` with a bare 444; 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 carries matrix discovery, the legacy `<hive>/matrix/*`
|
||||
# redirect and a redirect of everything else to the dashboard's name.
|
||||
# The gateway's own vhost, as a `services.hyperhive.gateway.vhosts`
|
||||
# declaration (rendered by ./declared-vhosts.nix): the `_` default server,
|
||||
# which answers unmatched `Host` with a bare 444.
|
||||
#
|
||||
# Scope: a swarm service declares its own vhost and dns name in its own
|
||||
# module, the same way. That keeps a service's gateway surface next to
|
||||
# the service, and keeps this file to the surface the hive itself serves.
|
||||
#
|
||||
# Pure function — called from ./default.nix with the outer-scope config
|
||||
# values as arguments; returns `{ vhosts }`.
|
||||
# Scope: a service declares its own vhost and dns name in its own module,
|
||||
# the same way — the hive's dashboard and hive domain in
|
||||
# ../hive-c0re/vhosts.nix, matrix discovery in
|
||||
# ../glue-matrix-hive-discovery.nix. That keeps a service's gateway surface
|
||||
# next to the service, and keeps this file to what the gateway itself
|
||||
# serves.
|
||||
{
|
||||
lib,
|
||||
cfg, # services.hyperhive.gateway
|
||||
matrixCfg, # services.hyperhive.swarm.matrix — what the homeserver IS
|
||||
matrixDeployCfg, # services.hyperhive.deploy.matrix — what THIS host runs
|
||||
hyperhiveDomain,
|
||||
dashboardDomain, # services.hyperhive.swarm.dashboardDomain
|
||||
autheliaDomain, # services.hyperhive.swarm.authelia.domain
|
||||
runsHive, # deploy.hive-controller.enable — whether hive-c0re runs here
|
||||
dashboardDist,
|
||||
swaggerUiTheme, # nix/packages/swagger-ui-theme.nix: has index.html + hyperhive-theme.css
|
||||
errorPages, # ./error-pages.nix: { notFound, unreachable, ssoUnavailable }
|
||||
gwLib, # `services.hyperhive.gateway.lib` — ./vhost-lib.nix's kit, via the option
|
||||
}:
|
||||
let
|
||||
# Read through `gwLib` (the published option) rather than importing
|
||||
# ./vhost-lib.nix directly, so the headers on the matrix discovery
|
||||
# locations are the ones every other vhost gets.
|
||||
inherit (gwLib) securityHeaders;
|
||||
|
||||
# Public-facing scheme + port-suffix for URLs the gateway
|
||||
# mints into responses (well-known JSON, the deprecated
|
||||
# `<hive>/matrix/*` 301 redirect, future absolute-URL needs):
|
||||
# always `https://<host>` (matrix-spec compliance) — the canonical
|
||||
# 443 elides the port. See `docs/networking/gateway.md` ("Self-signed TLS").
|
||||
publicScheme = "https";
|
||||
publicPort = cfg.httpsPort;
|
||||
publicPortSuffix = if publicPort == 443 then "" else ":${toString publicPort}";
|
||||
|
||||
# `<hive>/matrix/*` → 301 → `gatewayHost`'s vhost/$1 (`chat.<swarm-domain>`
|
||||
# by default; legacy deep-link shim for the fluffychat sub-domain).
|
||||
# See `docs/networking/gateway.md`.
|
||||
matrixRedirectLocations =
|
||||
lib.optionalAttrs (matrixDeployCfg.enable && matrixCfg.gatewayHost != null)
|
||||
(
|
||||
let
|
||||
target = "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}";
|
||||
in
|
||||
{
|
||||
"/matrix/" = {
|
||||
extraConfig = ''
|
||||
rewrite ^/matrix/(.*)$ ${target}/$1 permanent;
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
# `.well-known/matrix/{client,server}` discovery JSON, pointing clients
|
||||
# and peers at `matrixCfg.gatewayHost`: the homeserver has no other
|
||||
# address. CORS `*` per matrix spec. The `m.server` port-8448 carve-out
|
||||
# is documented inline. See `docs/networking/gateway.md`.
|
||||
wellKnownLocations = lib.optionalAttrs (matrixDeployCfg.enable && matrixCfg.gatewayHost != null) (
|
||||
let
|
||||
clientBaseUrl = "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}";
|
||||
# `m.server` is NOT a URL: per the matrix server-server spec
|
||||
# (Resolving Server Names) a delegated host with NO port resolves
|
||||
# to the federation default 8448 (after the SRV check) — the
|
||||
# https-implies-443 rule does NOT apply here. So the port must be
|
||||
# explicit even when it's the HTTPS default; `publicPortSuffix`
|
||||
# (which drops :443) is right for the client base_url above but
|
||||
# wrong for federation delegation. Without this, peers federate to
|
||||
# <gatewayHost>:8448 (closed) while the endpoint actually lives on
|
||||
# the gateway's 443 vhost. See docs/networking/gateway.md discovery flow.
|
||||
serverHostPort = "${matrixCfg.gatewayHost}:${toString publicPort}";
|
||||
in
|
||||
{
|
||||
"= /.well-known/matrix/client" = {
|
||||
extraConfig = ''
|
||||
default_type application/json;
|
||||
${securityHeaders}
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
return 200 '{"m.homeserver":{"base_url":"${clientBaseUrl}"}}';
|
||||
'';
|
||||
};
|
||||
"= /.well-known/matrix/server" = {
|
||||
extraConfig = ''
|
||||
default_type application/json;
|
||||
return 200 '{"m.server":"${serverHostPort}"}';
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
# `/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.
|
||||
# The catch-all, and now *only* a catch-all: anything whose `Host`
|
||||
# matches no vhost gets an immediate 444 (close without a response)
|
||||
# rather than being served the hive's dashboard.
|
||||
#
|
||||
# ⚠️ `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://${cfg.upstreamHost}:${toString cfg.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://${cfg.upstreamHost}:${toString cfg.upstreamPort}";
|
||||
};
|
||||
};
|
||||
|
||||
# Swagger UI: nginx hosts the FULL themed dist (`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/" below — that's
|
||||
# the one thing that has to stay dynamic.
|
||||
# `_` is the idiomatic spelling because it is not a legal hostname,
|
||||
# so it can never match a request by name — it serves traffic solely
|
||||
# by being `default_server`.
|
||||
#
|
||||
# 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 = "${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
|
||||
# ./vhost-lib.nix.
|
||||
recommendedProxySettings = false;
|
||||
extraConfig = ''
|
||||
internal;
|
||||
${gwLib.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
|
||||
{
|
||||
vhosts = {
|
||||
# The catch-all, and now *only* a catch-all: anything whose `Host`
|
||||
# matches no vhost gets an immediate 444 (close without a response)
|
||||
# rather than being served the hive's dashboard.
|
||||
#
|
||||
# `_` is the idiomatic spelling because it is not a legal hostname,
|
||||
# so it can never match a request by name — it serves traffic solely
|
||||
# by being `default_server`.
|
||||
#
|
||||
# ⚠️ It still gets TLS attrs from the gateway. It listens on the https port, so a
|
||||
# client connecting by IP completes a TLS handshake *before* nginx
|
||||
# can look at `Host` and reject it; with no cert the vhost fails to
|
||||
# load. The certificate will not match what such a client asked for
|
||||
# — that is unavoidable and correct: nothing can present a valid
|
||||
# cert for a name the operator never issued one for.
|
||||
#
|
||||
# The gateway renders `default` at `mkDefault` priority: an operator with their own
|
||||
# `default = true` vhost must be able to win without fighting
|
||||
# priorities. The assertion in ./default.nix catches the case where
|
||||
# they add one *without* turning this off, which nginx would
|
||||
# otherwise only report at runtime as a failed config test.
|
||||
"_" = {
|
||||
default = true;
|
||||
# A response to attach headers to is exactly what this vhost refuses.
|
||||
securityHeaders = false;
|
||||
extraConfig = ''
|
||||
return 444;
|
||||
'';
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (runsHive && dashboardDomain != null) {
|
||||
# The dashboard, reachable by its own NAME under the swarm domain, where
|
||||
# authelia's session cookie reaches. Only where a hive runs: the
|
||||
# dashboard, agent routes and swagger UI proxy to hive-c0re, which runs
|
||||
# only with the hive controller, and the hive leaf carrying this name
|
||||
# exists only there.
|
||||
#
|
||||
# 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: matrix discovery, which federation
|
||||
# reads off the server name, and a redirect of every other path to the
|
||||
# dashboard's name, so a bookmarked dashboard or agent link still lands.
|
||||
${hyperhiveDomain} = {
|
||||
locations =
|
||||
matrixRedirectLocations
|
||||
// wellKnownLocations
|
||||
// {
|
||||
"/".extraConfig = ''
|
||||
return 301 ${publicScheme}://${dashboardDomain}${publicPortSuffix}$request_uri;
|
||||
'';
|
||||
};
|
||||
};
|
||||
# ⚠️ It still gets TLS attrs from the gateway. It listens on the https port, so a
|
||||
# client connecting by IP completes a TLS handshake *before* nginx
|
||||
# can look at `Host` and reject it; with no cert the vhost fails to
|
||||
# load. The certificate will not match what such a client asked for
|
||||
# — that is unavoidable and correct: nothing can present a valid
|
||||
# cert for a name the operator never issued one for.
|
||||
#
|
||||
# The gateway renders `default` at `mkDefault` priority: an operator with their own
|
||||
# `default = true` vhost must be able to win without fighting
|
||||
# priorities. The assertion in ./default.nix catches the case where
|
||||
# they add one *without* turning this off, which nginx would
|
||||
# otherwise only report at runtime as a failed config test.
|
||||
"_" = {
|
||||
default = true;
|
||||
# A response to attach headers to is exactly what this vhost refuses.
|
||||
securityHeaders = false;
|
||||
extraConfig = ''
|
||||
return 444;
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue