gateway: service modules declare their vhost under gateway.vhosts
Ten service modules (forge, matrix chat, authelia, grafana, otel, nats, swarm-ui, victorialogs, victoriametrics, bao UI) wrote services.nginx.virtualHosts themselves, each applying the gateway's listen set, per-name TLS attrs and security headers by hand. They now declare services.hyperhive.gateway.vhosts.<name> with only their own locations and options; ./declared-vhosts.nix renders it into the nginx vhost with listen, TLS and headers added. An eval assertion refuses a declared vhost that sets listen, addSSL, onlySSL, enableACME, useACMEHost, sslCertificate or sslCertificateKey, and one declared while the gateway is off. nats' own leaf is the new `certificate` field. The rendered nginx vhosts are unchanged.
This commit is contained in:
parent
7b50ae57e4
commit
ddb583cc22
17 changed files with 652 additions and 513 deletions
|
|
@ -179,165 +179,161 @@ in
|
|||
# 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."${apex}" =
|
||||
(builtins.removeAttrs (gatewayCfg.lib.tlsFor apex) [ "addSSL" ])
|
||||
# The gateway drops `addSSL` from a vhost that sets `forceSSL`.
|
||||
services.hyperhive.gateway.vhosts."${apex}" = {
|
||||
forceSSL = true;
|
||||
locations = {
|
||||
"/" = {
|
||||
root = "${deployCfg.swarm-controller.ui.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;
|
||||
'';
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (stylixThemeColors != null) {
|
||||
# Stylix theming (see the `let` block above): overrides just
|
||||
# this one file from `deployCfg.swarm-controller.ui.package`'s own static root, rather
|
||||
# than copying the whole tree to change one file inside it —
|
||||
# nginx resolves the more specific `=` exact match over the
|
||||
# `/` prefix root above, so this simply doesn't exist (falling
|
||||
# through to the package's own untouched colors.css) when
|
||||
# there's no active palette. Same `auth_request` gate as every
|
||||
# other location here — no reason for this one path to have a
|
||||
# different failure mode than the page that loads it.
|
||||
"= /static/colors.css" = {
|
||||
alias = "${themedColorsCss stylixThemeColors}";
|
||||
extraConfig = swarmAuthRequest;
|
||||
};
|
||||
}
|
||||
// {
|
||||
forceSSL = true;
|
||||
listen = gatewayCfg.lib.listen;
|
||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
||||
locations = {
|
||||
"/" = {
|
||||
root = "${deployCfg.swarm-controller.ui.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;
|
||||
'';
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (stylixThemeColors != null) {
|
||||
# Stylix theming (see the `let` block above): overrides just
|
||||
# this one file from `deployCfg.swarm-controller.ui.package`'s own static root, rather
|
||||
# than copying the whole tree to change one file inside it —
|
||||
# nginx resolves the more specific `=` exact match over the
|
||||
# `/` prefix root above, so this simply doesn't exist (falling
|
||||
# through to the package's own untouched colors.css) when
|
||||
# there's no active palette. Same `auth_request` gate as every
|
||||
# other location here — no reason for this one path to have a
|
||||
# different failure mode than the page that loads it.
|
||||
"= /static/colors.css" = {
|
||||
alias = "${themedColorsCss stylixThemeColors}";
|
||||
extraConfig = swarmAuthRequest;
|
||||
};
|
||||
}
|
||||
// {
|
||||
# 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:${deployCfg.swarm-controller.socketPath}:";
|
||||
# `Remote-User` from authelia's answer, set here so it replaces
|
||||
# any the client sent: swarm-controller takes it as the sender of
|
||||
# the text the operator types to an agent (`agent_input.rs`).
|
||||
# A refused caller that sent an `Authorization` header (`swarmctl
|
||||
# --swarm`) gets the 401 itself; a browser still gets the login.
|
||||
# The bearer token stops here and never reaches the controller.
|
||||
extraConfig = ''
|
||||
${authRequest}
|
||||
error_page 401 = @swarm_api_unauthorized;
|
||||
auth_request_set $authelia_user $upstream_http_remote_user;
|
||||
proxy_set_header Remote-User $authelia_user;
|
||||
proxy_set_header Authorization "";
|
||||
# The SSE streams (terminal, agent state, subagent terminal)
|
||||
# must not sit in nginx's buffer. The controller's 15s
|
||||
# keep-alive keeps them under nginx's 60s default read timeout.
|
||||
proxy_buffering off;
|
||||
'';
|
||||
};
|
||||
"@swarm_api_unauthorized".extraConfig = ''
|
||||
if ($http_authorization) {
|
||||
return 401;
|
||||
}
|
||||
return 302 ${loginRedirect};
|
||||
# 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:${deployCfg.swarm-controller.socketPath}:";
|
||||
# `Remote-User` from authelia's answer, set here so it replaces
|
||||
# any the client sent: swarm-controller takes it as the sender of
|
||||
# the text the operator types to an agent (`agent_input.rs`).
|
||||
# A refused caller that sent an `Authorization` header (`swarmctl
|
||||
# --swarm`) gets the 401 itself; a browser still gets the login.
|
||||
# The bearer token stops here and never reaches the controller.
|
||||
extraConfig = ''
|
||||
${authRequest}
|
||||
error_page 401 = @swarm_api_unauthorized;
|
||||
auth_request_set $authelia_user $upstream_http_remote_user;
|
||||
proxy_set_header Remote-User $authelia_user;
|
||||
proxy_set_header Authorization "";
|
||||
# The SSE streams (terminal, agent state, subagent terminal)
|
||||
# must not sit in nginx's buffer. The controller's 15s
|
||||
# keep-alive keeps them under nginx's 60s default read timeout.
|
||||
proxy_buffering off;
|
||||
'';
|
||||
};
|
||||
"@swarm_api_unauthorized".extraConfig = ''
|
||||
if ($http_authorization) {
|
||||
return 401;
|
||||
}
|
||||
return 302 ${loginRedirect};
|
||||
'';
|
||||
# ⚠️ THE ONE PROXIED LOCATION ON THIS VHOST WITH NO `auth_request`,
|
||||
# and that is deliberate rather than an omission.
|
||||
#
|
||||
# A forge webhook is a machine POST carrying an HMAC signature
|
||||
# and no session cookie. An authelia auth-request subrequest
|
||||
# authenticates a *browser session*; there is nothing here for it
|
||||
# to check, so guarding this location would not make it safer, it
|
||||
# would make it permanently unreachable.
|
||||
#
|
||||
# What replaces it: swarm-controller verifies the
|
||||
# `X-Hub-Signature-256` HMAC over the raw body before looking at
|
||||
# anything else, and answers 401 on any mismatch. That check is
|
||||
# the access control for this path — see the `webhook` module.
|
||||
#
|
||||
# Scoped to `/webhook/forge/` rather than `/webhook/` so the
|
||||
# carve-out is exactly as wide as the endpoint that justifies it:
|
||||
# a future `/webhook/<something-else>` does not inherit the
|
||||
# bypass by living under a shared prefix.
|
||||
"/webhook/forge/" = {
|
||||
proxyPass = "http://unix:${deployCfg.swarm-controller.socketPath}:";
|
||||
};
|
||||
# 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/;
|
||||
'';
|
||||
};
|
||||
# Session identity for the header's profile menu: initials
|
||||
# avatar + "who is this" line. `auth_request` tells the
|
||||
# browser nothing about *who* (only `/api/` passes it on, to the
|
||||
# controller), so the frontend has no other way to learn this.
|
||||
# Same-origin proxy straight to authelia's own `GET /api/user/info`
|
||||
# (session-cookie authenticated) rather than new
|
||||
# swarm-controller code: the cookie is already valid here (the
|
||||
# session cookie's domain is the swarm's, shared across every
|
||||
# vhost under it — see swarm-authelia.nix), so this is a pure
|
||||
# pass-through with nothing for a daemon to add.
|
||||
#
|
||||
# `=` exact match, not a prefix, so proxy_pass's own URI part
|
||||
# (`/api/user/info`) REPLACES the matched request URI rather
|
||||
# than being appended to it — same substitution shape as
|
||||
# `/__hive_authelia` below, just not `internal` since the
|
||||
# frontend calls this one directly.
|
||||
"= /api/whoami" = {
|
||||
proxyPass = "https://${autheliaCfg.domain}/api/user/info";
|
||||
# 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 = ''
|
||||
${swarmAuthRequest}
|
||||
${gatewayCfg.lib.verifiedProxyTo autheliaCfg.domain}
|
||||
'';
|
||||
};
|
||||
"/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 = "https://${autheliaCfg.domain}/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 autheliaCfg.domain}
|
||||
# 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;
|
||||
'';
|
||||
# ⚠️ THE ONE PROXIED LOCATION ON THIS VHOST WITH NO `auth_request`,
|
||||
# and that is deliberate rather than an omission.
|
||||
#
|
||||
# A forge webhook is a machine POST carrying an HMAC signature
|
||||
# and no session cookie. An authelia auth-request subrequest
|
||||
# authenticates a *browser session*; there is nothing here for it
|
||||
# to check, so guarding this location would not make it safer, it
|
||||
# would make it permanently unreachable.
|
||||
#
|
||||
# What replaces it: swarm-controller verifies the
|
||||
# `X-Hub-Signature-256` HMAC over the raw body before looking at
|
||||
# anything else, and answers 401 on any mismatch. That check is
|
||||
# the access control for this path — see the `webhook` module.
|
||||
#
|
||||
# Scoped to `/webhook/forge/` rather than `/webhook/` so the
|
||||
# carve-out is exactly as wide as the endpoint that justifies it:
|
||||
# a future `/webhook/<something-else>` does not inherit the
|
||||
# bypass by living under a shared prefix.
|
||||
"/webhook/forge/" = {
|
||||
proxyPass = "http://unix:${deployCfg.swarm-controller.socketPath}:";
|
||||
};
|
||||
# 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/;
|
||||
'';
|
||||
};
|
||||
# Session identity for the header's profile menu: initials
|
||||
# avatar + "who is this" line. `auth_request` tells the
|
||||
# browser nothing about *who* (only `/api/` passes it on, to the
|
||||
# controller), so the frontend has no other way to learn this.
|
||||
# Same-origin proxy straight to authelia's own `GET /api/user/info`
|
||||
# (session-cookie authenticated) rather than new
|
||||
# swarm-controller code: the cookie is already valid here (the
|
||||
# session cookie's domain is the swarm's, shared across every
|
||||
# vhost under it — see swarm-authelia.nix), so this is a pure
|
||||
# pass-through with nothing for a daemon to add.
|
||||
#
|
||||
# `=` exact match, not a prefix, so proxy_pass's own URI part
|
||||
# (`/api/user/info`) REPLACES the matched request URI rather
|
||||
# than being appended to it — same substitution shape as
|
||||
# `/__hive_authelia` below, just not `internal` since the
|
||||
# frontend calls this one directly.
|
||||
"= /api/whoami" = {
|
||||
proxyPass = "https://${autheliaCfg.domain}/api/user/info";
|
||||
# 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 = ''
|
||||
${swarmAuthRequest}
|
||||
${gatewayCfg.lib.verifiedProxyTo autheliaCfg.domain}
|
||||
'';
|
||||
};
|
||||
"/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 = "https://${autheliaCfg.domain}/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 autheliaCfg.domain}
|
||||
# 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