authelia refuses to authorize a non-https target: the auth subrequest for `X-Original-URL: http://…` answers 400, and nginx's auth_request only understands 2xx/401/403, so a plain-http visit died as "auth request unexpected status: 400" — a bare 400 in the browser with no hint that a login exists. The shared vhostListen binds :80 as well as :443 and every vhost used addSSL, so the door was open on a port the lock cannot work on. That is harmless for forge and matrix, where http is merely insecure rather than broken, so the asymmetry stays local to the one vhost whose correctness depends on the scheme. removeAttrs because nixos asserts on a vhost declaring both addSSL and forceSSL. Measured against the pinned binary rather than reasoned about: X-Original-URL: http://constellation…/ -> 400 Bad Request X-Original-URL: https://constellation…/ -> 401 + Location: auth…?rd=…
537 lines
22 KiB
Nix
537 lines
22 KiB
Nix
# nginx virtual-host tree for the gateway: the `_` default server
|
|
# (dashboard, per-agent routing, matrix discovery), the forge, matrix
|
|
# and authelia sub-domain vhosts, and the Accept-header SPA map for the
|
|
# matrix GUI. Pure function — called from ./default.nix with the
|
|
# outer-scope config values as arguments; returns
|
|
# `{ virtualHosts, appendHttpConfig }`.
|
|
{
|
|
lib,
|
|
cfg, # services.hyperhive.gateway
|
|
forgeCfg,
|
|
matrixCfg,
|
|
autheliaCfg, # services.hyperhive.swarm.authelia
|
|
uiCfg, # services.hyperhive.swarm.ui
|
|
hyperhiveDomain,
|
|
dashboardDist,
|
|
swaggerUiTheme, # nix/packages/swagger-ui-theme.nix: has index.html + hyperhive-theme.css
|
|
errorPages, # ./error-pages.nix: { notFound, unreachable, unauthorized, ssoUnavailable }
|
|
tlsCert,
|
|
tlsKey,
|
|
svcCert, # swarm-services leaf, for names the hive CA cannot sign
|
|
svcKey,
|
|
swarmServiceDomains, # which vhosts those are (../swarm.nix derives it)
|
|
}:
|
|
let
|
|
# The gateway always terminates TLS: self-signed is the implicit
|
|
# floor when neither `tls.certDir` nor ACME is set, so there is no
|
|
# http-only mode. Listen addresses every vhost shares — plain http
|
|
# on `cfg.port` plus TLS on `cfg.httpsPort`. See `docs/gateway.md`
|
|
# ("TLS modes").
|
|
vhostListen = [
|
|
{
|
|
addr = "0.0.0.0";
|
|
port = cfg.port;
|
|
}
|
|
{
|
|
addr = "0.0.0.0";
|
|
port = cfg.httpsPort;
|
|
ssl = true;
|
|
}
|
|
];
|
|
# nixos `services.nginx.virtualHosts.<name>` ssl attrs merged
|
|
# into each vhost. For ACME mode: `enableACME` + `addSSL` —
|
|
# NixOS's ACME integration manages the cert lifecycle and sets
|
|
# ssl_certificate automatically. For self-signed / certDir:
|
|
# explicit cert paths.
|
|
vhostTls =
|
|
if cfg.tls.acme.enable then
|
|
{
|
|
addSSL = true;
|
|
enableACME = true;
|
|
}
|
|
else
|
|
{
|
|
addSSL = true;
|
|
sslCertificate = tlsCert;
|
|
sslCertificateKey = tlsKey;
|
|
};
|
|
|
|
# TLS attrs for one vhost, by name. A swarm service's name may sit
|
|
# outside this hive's domain — and then the hive CA is
|
|
# name-constrained out of it, so its vhost must serve the
|
|
# swarm-services leaf instead. Everything else keeps the hive leaf.
|
|
#
|
|
# Only in self-signed mode: with ACME or an operator cert there is a
|
|
# single issuer that already covers every name, and a second pair
|
|
# would be a cert nobody asked for.
|
|
vhostTlsFor =
|
|
host:
|
|
if !cfg.tls.acme.enable && cfg.tls.certDir == null && builtins.elem host swarmServiceDomains then
|
|
{
|
|
addSSL = true;
|
|
sslCertificate = svcCert;
|
|
sslCertificateKey = svcKey;
|
|
}
|
|
else
|
|
vhostTls;
|
|
|
|
# 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/gateway.md` ("Self-signed TLS").
|
|
publicScheme = "https";
|
|
publicPort = cfg.httpsPort;
|
|
publicPortSuffix = if publicPort == 443 then "" else ":${toString publicPort}";
|
|
|
|
# Security headers added at the server scope on every vhost.
|
|
# nginx's add_header inheritance rule: a location that defines its
|
|
# own add_header does NOT inherit the server-level ones. Any
|
|
# location with its own add_header (e.g. CORS on /.well-known or
|
|
# /_matrix/) must repeat the security headers explicitly — see those
|
|
# locations below. HTML-serving and proxy locations that carry no
|
|
# add_header of their own pick these up from the server scope
|
|
# automatically.
|
|
hstsDirectives = lib.concatStringsSep "; " (
|
|
[ "max-age=${toString cfg.hsts.maxAge}" ]
|
|
++ lib.optional cfg.hsts.includeSubDomains "includeSubDomains"
|
|
);
|
|
securityHeaders = ''
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
${lib.optionalString cfg.hsts.enable ''add_header Strict-Transport-Security "${hstsDirectives}" always;''}
|
|
'';
|
|
|
|
# Forge sub-domain vhost. `server_name = forge.domain`, proxies
|
|
# all `/` → forgejo. Tuned for git: `client_max_body_size 1G`,
|
|
# `proxy_read_timeout 1h` (multi-GB clones). SSH stays direct on
|
|
# `forge.sshPort`. See `docs/gateway.md`. Empty attrset when the
|
|
# forge isn't behind the gateway.
|
|
forgeVhost = lib.optionalAttrs (forgeCfg.behindGateway or false) {
|
|
"${forgeCfg.domain}" = (vhostTlsFor forgeCfg.domain) // {
|
|
listen = vhostListen;
|
|
extraConfig = securityHeaders;
|
|
locations."/" = {
|
|
proxyPass = "http://127.0.0.1:${toString forgeCfg.httpPort}/";
|
|
proxyWebsockets = true;
|
|
extraConfig = ''
|
|
proxy_buffering off;
|
|
client_max_body_size 1G;
|
|
proxy_read_timeout 1h;
|
|
proxy_send_timeout 1h;
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
# 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).
|
|
#
|
|
# ⚠️ `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.
|
|
swarmUiVhost = lib.optionalAttrs uiCfg.enable {
|
|
"${uiCfg.domain}" = (builtins.removeAttrs (vhostTlsFor uiCfg.domain) [ "addSSL" ]) // {
|
|
forceSSL = true;
|
|
listen = vhostListen;
|
|
extraConfig = securityHeaders;
|
|
locations = {
|
|
"/" = {
|
|
root = "${uiCfg.package}";
|
|
extraConfig = ''
|
|
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;
|
|
# 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;
|
|
'';
|
|
};
|
|
# 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;
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
# Matrix sub-domain vhost. `server_name = matrixCfg.gatewayHost`.
|
|
# `/_matrix/*` → tuwunel (CORS *, 50M body cap, 1h long-poll
|
|
# timeout). `/` serves fluffychat or 404 if GUI off. nginx
|
|
# longer-prefix-wins puts `/_matrix/` ahead of `/`. See
|
|
# `docs/gateway.md`. Empty attrset when matrix has no gateway host.
|
|
matrixVhost = lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) {
|
|
"${matrixCfg.gatewayHost}" = (vhostTlsFor matrixCfg.gatewayHost) // {
|
|
listen = vhostListen;
|
|
extraConfig = securityHeaders;
|
|
locations = {
|
|
"/_matrix/" = {
|
|
proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}";
|
|
proxyWebsockets = true;
|
|
extraConfig = ''
|
|
proxy_buffering off;
|
|
client_max_body_size 50M;
|
|
proxy_read_timeout 1h;
|
|
proxy_send_timeout 1h;
|
|
${securityHeaders}
|
|
add_header Access-Control-Allow-Origin *;
|
|
'';
|
|
};
|
|
}
|
|
// lib.optionalAttrs (matrixCfg.gui.enable) (
|
|
{
|
|
# fluffychat at sub-domain root, SPA-fallback via
|
|
# the Accept-header `$matrix_spa_target` map.
|
|
"/" = {
|
|
alias = "${matrixCfg.gui.package}/";
|
|
extraConfig = ''
|
|
try_files $uri $uri/ $matrix_spa_target =404;
|
|
'';
|
|
};
|
|
}
|
|
// {
|
|
# FluffyChat boot-config pre-fill so the client's
|
|
# `.well-known/matrix/client` lookup hits the
|
|
# right delegation endpoint. `domain` is required, so
|
|
# this is always present.
|
|
"= /config.json" = {
|
|
extraConfig = ''
|
|
default_type application/json;
|
|
return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}';
|
|
'';
|
|
};
|
|
}
|
|
)
|
|
// lib.optionalAttrs (!matrixCfg.gui.enable) {
|
|
"/" = {
|
|
return = "404";
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
# `<hive>/matrix/*` → 301 → `matrix.<hive>/$1` (legacy deep-link
|
|
# shim during the fluffychat sub-domain move). See `docs/gateway.md`.
|
|
matrixRedirectLocations =
|
|
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gui.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. Points
|
|
# clients at `matrixCfg.gatewayHost` when set; falls back to direct
|
|
# `<hive>:<httpPort>`. CORS `*` per matrix spec. The `m.server`
|
|
# port-8448 carve-out is documented inline. See `docs/gateway.md`.
|
|
wellKnownLocations = lib.optionalAttrs matrixCfg.enable (
|
|
let
|
|
clientBaseUrl =
|
|
if matrixCfg.gatewayHost != null then
|
|
"${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}"
|
|
else
|
|
"${publicScheme}://${hyperhiveDomain}:${toString matrixCfg.httpPort}";
|
|
# `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/gateway.md discovery flow.
|
|
serverHostPort =
|
|
if matrixCfg.gatewayHost != null then
|
|
"${matrixCfg.gatewayHost}:${toString publicPort}"
|
|
else
|
|
"${hyperhiveDomain}:${toString matrixCfg.httpPort}";
|
|
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;
|
|
'';
|
|
};
|
|
};
|
|
|
|
# Shared auth block — separate locations don't inherit auth_basic, so
|
|
# each dashboard location (`/`, `/api/`) needs it or that surface is
|
|
# unauthed. `/webhook/` is intentionally excluded: Forgejo cannot
|
|
# send HTTP Basic credentials with webhook deliveries, and the HMAC
|
|
# secret (`X-Hub-Signature-256`) protects those endpoints instead.
|
|
dashboardAuth = lib.optionalString cfg.auth.enable ''
|
|
auth_basic "${cfg.auth.realm}";
|
|
auth_basic_user_file /var/lib/hive-gateway/conf/gateway.htpasswd;
|
|
# `=401` keeps the status 401 so the login dialog shows; the
|
|
# internal page explains `hivectl gateway create-user`.
|
|
error_page 401 =401 /__hive_auth_unauthorized;
|
|
'';
|
|
|
|
# Dashboard: nginx static-serves the dist, c0re is API-only. Routing
|
|
# is by PATH, never content-type. c0re serves exactly three prefixes —
|
|
# `/api/` (all dashboard data + actions + the SSE streams), `/webhook/`
|
|
# (knowledge push + config-PR approval triggers, HMAC-guarded), 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 + /webhook + /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}
|
|
'';
|
|
};
|
|
"/webhook/" = {
|
|
# No dashboardAuth here: Forgejo cannot send HTTP Basic credentials
|
|
# with webhook deliveries. HMAC (X-Hub-Signature-256) is the auth
|
|
# for these endpoints; hive-c0re verifies it in the handler.
|
|
proxyPass = "http://${cfg.upstreamHost}:${toString cfg.upstreamPort}";
|
|
};
|
|
"/health/" = {
|
|
# No dashboardAuth here either, for a different reason than
|
|
# /webhook/: an external uptime monitor generally can't do
|
|
# interactive HTTP Basic. 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 — per the operator's shape: "core
|
|
# should not need the swagger ui at all if it is hosted in gateway"
|
|
# / "core only hosts the json". Only `/api/openapi.json`
|
|
# (the live-generated spec `index.html` fetches; not under this
|
|
# prefix) still proxies to c0re via "/api/" below — 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 now needs to cover
|
|
# every file in the tree (bundle.js, maps, favicons, …), not just
|
|
# our 2 override files — hive-c0re no longer serves any of this as
|
|
# a fallback once its own `utoipa-swagger-ui` mount is removed.
|
|
# `= /api/docs` (no trailing slash) issues the same redirect
|
|
# `utoipa-swagger-ui`'s router used to: that mount is going away
|
|
# too, so nginx has to own it now, or the H0M3 hub's own `/api/docs`
|
|
# link (no trailing slash) would 404 once hive-c0re drops the route.
|
|
swaggerUiLocations = {
|
|
"= /api/docs" = {
|
|
extraConfig = ''
|
|
return 301 /api/docs/;
|
|
'';
|
|
};
|
|
"/api/docs/" = {
|
|
alias = "${swaggerUiTheme}/";
|
|
extraConfig = ''
|
|
index index.html;
|
|
${dashboardAuth}
|
|
'';
|
|
};
|
|
};
|
|
in
|
|
{
|
|
# Accept-header SPA map for the matrix GUI only (see docs/gateway.md
|
|
# "SPA fallback"): text/html → index.html, else a sentinel so
|
|
# try_files falls through to 404. The dashboard doesn't use an
|
|
# Accept-header map — it routes by path (see dashboardProxyLocation).
|
|
appendHttpConfig = lib.optionalString (matrixCfg.enable && matrixCfg.gui.enable) ''
|
|
map $http_accept $matrix_spa_target {
|
|
default "/__matrix_spa_no_html_fallback";
|
|
"~*text/html" "/index.html";
|
|
}
|
|
'';
|
|
|
|
virtualHosts = {
|
|
"_" = vhostTls // {
|
|
listen = vhostListen;
|
|
locations =
|
|
matrixRedirectLocations
|
|
// wellKnownLocations
|
|
// agentLocations
|
|
// dashboardProxyLocation
|
|
// swaggerUiLocations
|
|
// lib.optionalAttrs cfg.auth.enable {
|
|
# Internal-only target for the 401 error_page above.
|
|
# `internal` prevents direct client access; `alias` serves
|
|
# the pre-built HTML from the Nix store.
|
|
"= /__hive_auth_unauthorized" = {
|
|
extraConfig = ''
|
|
internal;
|
|
alias ${errorPages.unauthorized};
|
|
default_type text/html;
|
|
'';
|
|
};
|
|
};
|
|
# 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 = securityHeaders + ''
|
|
include /var/lib/hive-gateway/conf/agents.conf;
|
|
'';
|
|
};
|
|
}
|
|
// forgeVhost
|
|
// autheliaVhost
|
|
// matrixVhost
|
|
// swarmUiVhost;
|
|
}
|