refactor: split hive-gateway into module dir, DHCP range into hive-network
This commit is contained in:
parent
ecaad48fad
commit
5f9af9fc97
8 changed files with 1143 additions and 1075 deletions
353
nix/modules/hive-gateway/vhosts.nix
Normal file
353
nix/modules/hive-gateway/vhosts.nix
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
# nginx virtual-host tree for the gateway container: the `_` default
|
||||
# server (dashboard, per-agent routing, matrix discovery), the forge
|
||||
# and matrix sub-domain vhosts, and the Accept-header SPA map for the
|
||||
# matrix GUI. Pure function — called from ./default.nix inside the
|
||||
# container config with the outer-scope config values as arguments;
|
||||
# returns `{ virtualHosts, appendHttpConfig }`.
|
||||
{
|
||||
lib,
|
||||
cfg, # services.hyperhive.gateway
|
||||
forgeCfg,
|
||||
matrixCfg,
|
||||
hyperhiveDomain,
|
||||
dashboardDist,
|
||||
errorPages, # ./error-pages.nix: { notFound, unreachable, unauthorized }
|
||||
tlsCert,
|
||||
tlsKey,
|
||||
}:
|
||||
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. Kept as a named binding for the vhost listen/ssl
|
||||
# wiring below.
|
||||
hasTls = true;
|
||||
# Listen addresses every vhost shares. Plain http on `cfg.port`
|
||||
# always; `cfg.httpsPort` with TLS sits beside it when TLS is
|
||||
# active (any mode). See `docs/gateway.md` ("TLS modes").
|
||||
vhostListen = [
|
||||
{
|
||||
addr = "0.0.0.0";
|
||||
port = cfg.port;
|
||||
}
|
||||
]
|
||||
++ lib.optional hasTls {
|
||||
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
|
||||
lib.optionalAttrs hasTls {
|
||||
addSSL = true;
|
||||
sslCertificate = tlsCert;
|
||||
sslCertificateKey = tlsKey;
|
||||
};
|
||||
|
||||
# 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).
|
||||
# When TLS is active (self-signed OR operator cert), prefer
|
||||
# `https://<host>` (matrix-spec compliance) — 443 elides the
|
||||
# port. Otherwise fall back to the plain-http listen with the
|
||||
# bare port. See `docs/gateway.md` ("Self-signed TLS").
|
||||
publicScheme = if hasTls then "https" else "http";
|
||||
publicPort = if hasTls then cfg.httpsPort else cfg.port;
|
||||
publicPortDefault = if hasTls then 443 else 80;
|
||||
publicPortSuffix = if publicPort == publicPortDefault 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}" = vhostTls // {
|
||||
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;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# 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}" = vhostTls // {
|
||||
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 `/run/hive-state/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 /run/hive-state/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 two prefixes —
|
||||
# `/api/` (all dashboard data + actions + the SSE streams) and
|
||||
# `/webhook/` (knowledge push + config-PR approval triggers, HMAC-
|
||||
# guarded) — 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) 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}";
|
||||
};
|
||||
};
|
||||
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
|
||||
// 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/hyperhive/gateway/agents.conf
|
||||
# on the host. The bind-mount at /run/hive-state/ exposes
|
||||
# that file here. nginx parses `include` at config-load
|
||||
# time so a reload (triggered by c0re via systemd-run
|
||||
# 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 /run/hive-state/agents.conf;
|
||||
'';
|
||||
};
|
||||
}
|
||||
// forgeVhost
|
||||
// matrixVhost;
|
||||
}
|
||||
Loading…
Reference in a new issue