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
|
|
@ -22,7 +22,7 @@ You rarely switch it on yourself. `gateway.enable` defaults to off, and every mo
|
||||||
| `<queue domain>/metrics` | the queue exporter's `/metrics` on loopback, behind an authelia `auth_request`; every other path, `/` included, returns `403`, so `/varz` and `/connz` are never reachable | `swarm-nats.nix`, the queue's host |
|
| `<queue domain>/metrics` | the queue exporter's `/metrics` on loopback, behind an authelia `auth_request`; every other path, `/` included, returns `403`, so `/varz` and `/connz` are never reachable | `swarm-nats.nix`, the queue's host |
|
||||||
| `grafana.<swarm>`, `metrics.<swarm>`, `logs.<swarm>`, `otel.<swarm>`, `bao.<swarm>` | the matching swarm service | that service's module → [`swarm/services.md`](../swarm/services.md) |
|
| `grafana.<swarm>`, `metrics.<swarm>`, `logs.<swarm>`, `otel.<swarm>`, `bao.<swarm>` | the matching swarm service | that service's module → [`swarm/services.md`](../swarm/services.md) |
|
||||||
|
|
||||||
A service module declares its vhost as `services.hyperhive.gateway.vhosts.<name>`: the locations, `forceSSL` and any `extraConfig` it needs. The gateway renders it into `services.nginx.virtualHosts.<name>` and adds the `listen` set, the certificate for that name and the [security headers](#security-headers) (ahead of the vhost's own `extraConfig`), and drops `addSSL` from a vhost that sets `forceSSL`. The eval fails for a vhost that sets `listen`, `addSSL`, `onlySSL`, `enableACME`, `useACMEHost`, `sslCertificate` or `sslCertificateKey` itself, and for any vhost declared while `gateway.enable` is false. A name with a certificate from outside the gateway's leaves (the queue's) sets `certificate = { cert; key; }` instead. `hive-gateway/vhosts.nix` declares the gateway's own vhosts (`_`, the dashboard and the hive domain) the same way; the catch-all sets `securityHeaders = false` because it sends no response, and the gateway renders a vhost's `default` at `mkDefault` priority.
|
A service module declares its vhost as `services.hyperhive.gateway.vhosts.<name>`: the locations, `forceSSL` and any `extraConfig` it needs. The gateway renders it into `services.nginx.virtualHosts.<name>` and adds the `listen` set, the certificate for that name and the [security headers](#security-headers) (ahead of the vhost's own `extraConfig`), and drops `addSSL` from a vhost that sets `forceSSL`. The eval fails for a vhost that sets `listen`, `addSSL`, `onlySSL`, `enableACME`, `useACMEHost`, `sslCertificate` or `sslCertificateKey` itself, and for any vhost declared while `gateway.enable` is false. A name with a certificate from outside the gateway's leaves (the queue's) sets `certificate = { cert; key; }` instead. `hive-gateway/vhosts.nix` declares the gateway's own catch-all (`_`) the same way, `hive-c0re/vhosts.nix` the dashboard and the hive domain, and `glue-matrix-hive-discovery.nix` adds matrix discovery to the hive domain where the homeserver runs. The catch-all sets `securityHeaders = false` because it sends no response, and the gateway renders a vhost's `default` at `mkDefault` priority.
|
||||||
|
|
||||||
**The hive's own vhosts**: the dashboard's, named `<name>.hives.<swarm>` (`swarm.dashboardDomain`), and one named for the hive domain. They exist only on a host that runs a hive (`deploy.hive-controller.enable`); a host without a hive has none of these rows:
|
**The hive's own vhosts**: the dashboard's, named `<name>.hives.<swarm>` (`swarm.dashboardDomain`), and one named for the hive domain. They exist only on a host that runs a hive (`deploy.hive-controller.enable`); a host without a hive has none of these rows:
|
||||||
|
|
||||||
|
|
@ -258,7 +258,7 @@ hive-c0re serves exactly two prefixes, so the dashboard routes by **path** — d
|
||||||
- `location /health/` → hive-c0re: liveness and readiness.
|
- `location /health/` → hive-c0re: liveness and readiness.
|
||||||
- `location /` → the dashboard dist (from the `servedFrontend` nix-store path) with `try_files $uri /index.html`.
|
- `location /` → the dashboard dist (from the `servedFrontend` nix-store path) with `try_files $uri /index.html`.
|
||||||
|
|
||||||
The gateway static-serves the dist while hive-c0re stays API-only, so a frontend-only change doesn't restart the core daemon. A new top-level c0re route prefix needs a matching `location` on the hive vhost (`hive-gateway/vhosts.nix`).
|
The gateway static-serves the dist while hive-c0re stays API-only, so a frontend-only change doesn't restart the core daemon. A new top-level c0re route prefix needs a matching `location` on the dashboard vhost (`hive-c0re/vhosts.nix`).
|
||||||
|
|
||||||
### Per-agent unix-socket upstream
|
### Per-agent unix-socket upstream
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ fn agent_url(name: &str) -> Option<String> {
|
||||||
/// The pure half of [`agent_url`], split out so the path-scheme formatting
|
/// The pure half of [`agent_url`], split out so the path-scheme formatting
|
||||||
/// is testable without an env var in the loop. `/agent/<name>/` is the
|
/// is testable without an env var in the loop. `/agent/<name>/` is the
|
||||||
/// gateway's real path scheme
|
/// gateway's real path scheme
|
||||||
/// (`nix/host-modules/hive-gateway/vhosts.nix`'s per-agent `agentLocations`
|
/// (`nix/host-modules/hive-c0re/vhosts.nix`'s per-agent `agentLocations`
|
||||||
/// block) — computed once here so a client (trollshell's sidebar, the
|
/// block) — computed once here so a client (trollshell's sidebar, the
|
||||||
/// dashboard) never has to derive it and silently drift if that scheme
|
/// dashboard) never has to derive it and silently drift if that scheme
|
||||||
/// ever changes.
|
/// ever changes.
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ use crate::lifecycle;
|
||||||
/// Root of the auto-generated `OpenAPI` spec, served raw at
|
/// Root of the auto-generated `OpenAPI` spec, served raw at
|
||||||
/// `/api/openapi.json` — see [`utoipa`]. Swagger UI itself (browsable at
|
/// `/api/openapi.json` — see [`utoipa`]. Swagger UI itself (browsable at
|
||||||
/// `/api/docs`) is nginx-hosted from the nix store (see
|
/// `/api/docs`) is nginx-hosted from the nix store (see
|
||||||
/// `nix/host-modules/hive-gateway/vhosts.nix`'s `swaggerUiLocations`).
|
/// `nix/host-modules/hive-c0re/vhosts.nix`'s `swaggerUiLocations`).
|
||||||
/// Only routes carrying a `#[utoipa::path(...)]` annotation show up in
|
/// Only routes carrying a `#[utoipa::path(...)]` annotation show up in
|
||||||
/// the spec — an unannotated route just doesn't appear, nothing breaks.
|
/// the spec — an unannotated route just doesn't appear, nothing breaks.
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
|
|
@ -91,7 +91,7 @@ struct AppState {
|
||||||
)]
|
)]
|
||||||
fn app(coord: Arc<Coordinator>) -> axum::Router {
|
fn app(coord: Arc<Coordinator>) -> axum::Router {
|
||||||
// API-only: the gateway static-serves the dashboard dist and proxies
|
// API-only: the gateway static-serves the dashboard dist and proxies
|
||||||
// non-static requests here (see `nix/host-modules/hive-gateway/vhosts.nix`).
|
// non-static requests here (see `nix/host-modules/hive-c0re/vhosts.nix`).
|
||||||
// Unmatched paths 404.
|
// Unmatched paths 404.
|
||||||
// No static fallback needed here either. Every route (SSE streams
|
// No static fallback needed here either. Every route (SSE streams
|
||||||
// included — `utoipa::path` can document an SSE body as an opaque
|
// included — `utoipa::path` can document an SSE body as an opaque
|
||||||
|
|
|
||||||
|
|
@ -223,8 +223,8 @@ pub const KNOWLEDGE_DIR: &str = "/var/lib/hyperhive/knowledge";
|
||||||
/// confinement; nothing else is doing that job.
|
/// confinement; nothing else is doing that job.
|
||||||
///
|
///
|
||||||
/// Sibling of the gateway's `tls/`, which already lived here.
|
/// Sibling of the gateway's `tls/`, which already lived here.
|
||||||
// nix: named by the gateway's nginx config (hive-gateway/vhosts.nix) and created by
|
// nix: named by the dashboard vhost (hive-c0re/vhosts.nix) and created by the gateway's
|
||||||
// its tmpfiles rules (hive-gateway/default.nix) — must match.
|
// tmpfiles rules (hive-gateway/default.nix) — must match.
|
||||||
pub const GATEWAY_CONF_DIR: &str = "/var/lib/hive-gateway/conf";
|
pub const GATEWAY_CONF_DIR: &str = "/var/lib/hive-gateway/conf";
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
./glue-logs-auth-bao-identity.nix
|
./glue-logs-auth-bao-identity.nix
|
||||||
./glue-metrics-auth-bao-identity.nix
|
./glue-metrics-auth-bao-identity.nix
|
||||||
./glue-matrix-ctl-bao-identity.nix
|
./glue-matrix-ctl-bao-identity.nix
|
||||||
|
./glue-matrix-hive-discovery.nix
|
||||||
./glue-matrix-oidc-client.nix
|
./glue-matrix-oidc-client.nix
|
||||||
./glue-nats-auth-bao-identity.nix
|
./glue-nats-auth-bao-identity.nix
|
||||||
./glue-nats-bao-identity.nix
|
./glue-nats-bao-identity.nix
|
||||||
|
|
|
||||||
81
nix/host-modules/glue-matrix-hive-discovery.nix
Normal file
81
nix/host-modules/glue-matrix-hive-discovery.nix
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Glue: serve the swarm homeserver's discovery on the hive domain's vhost
|
||||||
|
# wherever both run.
|
||||||
|
#
|
||||||
|
# ONE PAIRING PER FILE — matrix → hive domain vhost, and nothing else.
|
||||||
|
# Deleting this leaves federation and clients with no `.well-known/matrix/*`
|
||||||
|
# answer at the server name, and the legacy `<hive>/matrix/*` links 404.
|
||||||
|
#
|
||||||
|
# The locations sit on the hive domain because the matrix spec reads
|
||||||
|
# delegation off the *server name*: it is the hive answering "where is my
|
||||||
|
# homeserver", not the homeserver answering for itself (./hive-matrix.nix's
|
||||||
|
# vhost on `gatewayHost` carries the rest).
|
||||||
|
#
|
||||||
|
# ⚠️ The condition repeats the one ./hive-c0re/vhosts.nix declares the hive
|
||||||
|
# domain vhost under. With it looser, this file alone would declare that
|
||||||
|
# vhost, with discovery and no `/` redirect.
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
hyperhiveCfg = config.services.hyperhive;
|
||||||
|
gatewayCfg = hyperhiveCfg.gateway;
|
||||||
|
matrixCfg = hyperhiveCfg.swarm.matrix;
|
||||||
|
|
||||||
|
publicPort = gatewayCfg.httpsPort;
|
||||||
|
# The canonical 443 elides the port; see `docs/networking/gateway.md`
|
||||||
|
# ("Self-signed TLS").
|
||||||
|
homeserverUrl = "https://${matrixCfg.gatewayHost}${
|
||||||
|
lib.optionalString (publicPort != 443) ":${toString publicPort}"
|
||||||
|
}";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config =
|
||||||
|
lib.mkIf
|
||||||
|
(
|
||||||
|
hyperhiveCfg.deploy.matrix.enable
|
||||||
|
&& matrixCfg.gatewayHost != null
|
||||||
|
&& hyperhiveCfg.deploy.hive-controller.enable
|
||||||
|
&& gatewayCfg.enable
|
||||||
|
&& hyperhiveCfg.swarm.dashboardDomain != null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
services.hyperhive.gateway.vhosts.${hyperhiveCfg.domain}.locations = {
|
||||||
|
# `<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`.
|
||||||
|
"/matrix/" = {
|
||||||
|
extraConfig = ''
|
||||||
|
rewrite ^/matrix/(.*)$ ${homeserverUrl}/$1 permanent;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# `.well-known/matrix/{client,server}` discovery JSON, pointing
|
||||||
|
# clients and peers at `gatewayHost`: the homeserver has no other
|
||||||
|
# address. CORS `*` per matrix spec. See `docs/networking/gateway.md`.
|
||||||
|
"= /.well-known/matrix/client" = {
|
||||||
|
extraConfig = ''
|
||||||
|
default_type application/json;
|
||||||
|
${gatewayCfg.lib.securityHeaders}
|
||||||
|
add_header Access-Control-Allow-Origin *;
|
||||||
|
return 200 '{"m.homeserver":{"base_url":"${homeserverUrl}"}}';
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
# `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, unlike the client
|
||||||
|
# base_url above. Without it, peers federate to <gatewayHost>:8448
|
||||||
|
# (closed) while the endpoint actually lives on the gateway's 443
|
||||||
|
# vhost. See docs/networking/gateway.md discovery flow.
|
||||||
|
"= /.well-known/matrix/server" = {
|
||||||
|
extraConfig = ''
|
||||||
|
default_type application/json;
|
||||||
|
return 200 '{"m.server":"${matrixCfg.gatewayHost}:${toString publicPort}"}';
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
# The hive-c0re coordinator daemon (runs as the unprivileged
|
# The hive-c0re coordinator daemon (runs as the unprivileged
|
||||||
# `hive-core` user), socket-activated at /run/hyperhive/host.sock.
|
# `hive-core` user), socket-activated at /run/hyperhive/host.sock.
|
||||||
# Layout: ./options.nix (option declarations), ./theme.nix (stylix
|
# Layout: ./options.nix (option declarations), ./theme.nix (stylix
|
||||||
# frontend theming → `servedFrontend`), ./environment.nix (the daemon
|
# frontend theming → `servedFrontend`), ./vhosts.nix (its gateway vhosts),
|
||||||
# unit's env attrset). The root privileged helper it delegates to is
|
# ./environment.nix (the daemon unit's env attrset). The root privileged
|
||||||
# its own module (../hive-priv.nix).
|
# helper it delegates to is its own module (../hive-priv.nix).
|
||||||
{
|
{
|
||||||
pkgs,
|
pkgs,
|
||||||
lib,
|
lib,
|
||||||
|
|
@ -125,6 +125,7 @@ in
|
||||||
imports = [
|
imports = [
|
||||||
./options.nix
|
./options.nix
|
||||||
./theme.nix
|
./theme.nix
|
||||||
|
./vhosts.nix
|
||||||
# Hive-CA trust for this daemon's outbound TLS. The matrix provisioning
|
# Hive-CA trust for this daemon's outbound TLS. The matrix provisioning
|
||||||
# URL (`swarm.matrix.apiUrl`) defaults to https on the homeserver's
|
# URL (`swarm.matrix.apiUrl`) defaults to https on the homeserver's
|
||||||
# gateway vhost, which serves a leaf chaining to the store's pki root —
|
# gateway vhost, which serves a leaf chaining to the store's pki root —
|
||||||
|
|
|
||||||
213
nix/host-modules/hive-c0re/vhosts.nix
Normal file
213
nix/host-modules/hive-c0re/vhosts.nix
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
# 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;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
# namespace of its own.
|
# namespace of its own.
|
||||||
# Full vhost map + discovery flow + design rationale in
|
# Full vhost map + discovery flow + design rationale in
|
||||||
# `docs/networking/gateway.md`. Layout: ./options.nix (option declarations),
|
# `docs/networking/gateway.md`. Layout: ./options.nix (option declarations),
|
||||||
# ./vhosts.nix (the gateway's own nginx virtual hosts), ./declared-vhosts.nix
|
# ./vhosts.nix (the catch-all `_` vhost), ./declared-vhosts.nix
|
||||||
# (renders the vhosts service modules declare under `gateway.vhosts`), ./error-pages.nix
|
# (renders the vhosts service modules declare under `gateway.vhosts`), ./error-pages.nix
|
||||||
# (styled static pages), ./dns.nix + ./dnsmasq.nix (the resolver, which
|
# (styled static pages), ./dns.nix + ./dnsmasq.nix (the resolver, which
|
||||||
# has an enable of its own because a host can need hive names to resolve
|
# has an enable of its own because a host can need hive names to resolve
|
||||||
|
|
@ -24,8 +24,6 @@ let
|
||||||
# cert are exactly the names that cert is issued for, so both read the
|
# cert are exactly the names that cert is issued for, so both read the
|
||||||
# same list rather than each deciding what "a swarm service" means.
|
# same list rather than each deciding what "a swarm service" means.
|
||||||
swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains;
|
swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains;
|
||||||
matrixCfg = config.services.hyperhive.swarm.matrix;
|
|
||||||
matrixDeployCfg = config.services.hyperhive.deploy.matrix;
|
|
||||||
tlsStateDir = config.services.hyperhive.deploy.hive-controller.tls.stateDir;
|
tlsStateDir = config.services.hyperhive.deploy.hive-controller.tls.stateDir;
|
||||||
# The hive leaf exists only where a hive runs; ../hive-tls.nix issues it
|
# The hive leaf exists only where a hive runs; ../hive-tls.nix issues it
|
||||||
# under that same condition.
|
# under that same condition.
|
||||||
|
|
@ -48,16 +46,6 @@ let
|
||||||
lib.attrNames config.services.nginx.virtualHosts
|
lib.attrNames config.services.nginx.virtualHosts
|
||||||
);
|
);
|
||||||
|
|
||||||
# Dashboard SPA dist, static-served by nginx.
|
|
||||||
dashboardDist = "${config.services.hyperhive.c0re.servedFrontend}/dashboard";
|
|
||||||
|
|
||||||
# Full hyperhive-themed Swagger UI dist — nginx serves this whole
|
|
||||||
# tree straight from the store at /api/docs/, no hive-c0re fallback
|
|
||||||
# (see vhosts.nix's `swaggerUiLocations` and
|
|
||||||
# nix/packages/swagger-ui-{dist,theme}.nix). Own option under this
|
|
||||||
# module (not c0re's) — hive-c0re has no relationship to it.
|
|
||||||
swaggerUiTheme = cfg.swaggerUiTheme;
|
|
||||||
|
|
||||||
# Self-signed TLS is the implicit floor: when neither an operator cert
|
# Self-signed TLS is the implicit floor: when neither an operator cert
|
||||||
# (`tls.certDir`) nor ACME (`tls.acme.enable`) is configured, the gateway
|
# (`tls.certDir`) nor ACME (`tls.acme.enable`) is configured, the gateway
|
||||||
# serves a leaf the secret store issues (see hive-tls.nix). There is no
|
# serves a leaf the secret store issues (see hive-tls.nix). There is no
|
||||||
|
|
@ -85,17 +73,15 @@ let
|
||||||
svcCert = "${tlsDir}/swarm-services.pem";
|
svcCert = "${tlsDir}/swarm-services.pem";
|
||||||
svcKey = "${tlsDir}/swarm-services-key.pem";
|
svcKey = "${tlsDir}/swarm-services-key.pem";
|
||||||
|
|
||||||
# Styled static error pages. Built once here and reached two ways:
|
# Styled static error pages, reached through the published kit by any
|
||||||
# directly by ./vhosts.nix, and via the published kit by any service
|
# service module that aims an `error_page` at one.
|
||||||
# module that aims an `error_page` at one.
|
|
||||||
errorPages = import ./error-pages.nix { inherit pkgs; };
|
errorPages = import ./error-pages.nix { inherit pkgs; };
|
||||||
|
|
||||||
# The vhost construction kit (listen set / per-name TLS attrs /
|
# The vhost construction kit (listen set / per-name TLS attrs /
|
||||||
# security headers / error pages). Computed here, published as
|
# security headers / error pages). Computed here and published as
|
||||||
# `cfg.lib` below, and handed to ./vhosts.nix **as the published
|
# `cfg.lib` below, which ./declared-vhosts.nix and every service module
|
||||||
# value** — so the tree the gateway renders and the kit a service
|
# read, so the tree the gateway renders and the kit a service module
|
||||||
# module gets are the same object by construction, not by two call
|
# gets are the same object by construction.
|
||||||
# sites agreeing.
|
|
||||||
vhostLib = import ./vhost-lib.nix {
|
vhostLib = import ./vhost-lib.nix {
|
||||||
inherit
|
inherit
|
||||||
lib
|
lib
|
||||||
|
|
@ -113,23 +99,6 @@ let
|
||||||
# per-container copy.
|
# per-container copy.
|
||||||
caBundle = "${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/trust-bundle.pem";
|
caBundle = "${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/trust-bundle.pem";
|
||||||
};
|
};
|
||||||
|
|
||||||
ownVhosts = import ./vhosts.nix {
|
|
||||||
gwLib = cfg.lib;
|
|
||||||
inherit (config.services.hyperhive.swarm) dashboardDomain;
|
|
||||||
autheliaDomain = config.services.hyperhive.swarm.authelia.domain;
|
|
||||||
inherit
|
|
||||||
lib
|
|
||||||
cfg
|
|
||||||
errorPages
|
|
||||||
matrixCfg
|
|
||||||
matrixDeployCfg
|
|
||||||
hyperhiveDomain
|
|
||||||
runsHive
|
|
||||||
dashboardDist
|
|
||||||
swaggerUiTheme
|
|
||||||
;
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
|
@ -145,8 +114,8 @@ in
|
||||||
# file's `let` and are not option surface.
|
# file's `let` and are not option surface.
|
||||||
services.hyperhive.gateway.lib = vhostLib;
|
services.hyperhive.gateway.lib = vhostLib;
|
||||||
|
|
||||||
# The gateway's own vhosts, rendered the way every service's are.
|
# The gateway's own vhost, rendered the way every service's are.
|
||||||
services.hyperhive.gateway.vhosts = ownVhosts.vhosts;
|
services.hyperhive.gateway.vhosts = import ./vhosts.nix;
|
||||||
|
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
|
|
@ -256,12 +225,6 @@ in
|
||||||
"r /var/lib/hive-gateway/conf/gateway.htpasswd"
|
"r /var/lib/hive-gateway/conf/gateway.htpasswd"
|
||||||
];
|
];
|
||||||
|
|
||||||
# The dashboard's name, for agents on the bridge and (with
|
|
||||||
# `localHostsEntry`) the host's own browser.
|
|
||||||
services.hyperhive.gateway.localNames = lib.optional (
|
|
||||||
runsHive && config.services.hyperhive.swarm.dashboardDomain != null
|
|
||||||
) config.services.hyperhive.swarm.dashboardDomain;
|
|
||||||
|
|
||||||
# ACME (Let's Encrypt) integration. nginx vhosts set
|
# ACME (Let's Encrypt) integration. nginx vhosts set
|
||||||
# `enableACME = true` via the vhost builder; this provides the
|
# `enableACME = true` via the vhost builder; this provides the
|
||||||
# shared ACME config (acceptTerms + email).
|
# shared ACME config (acceptTerms + email).
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# special-cased, all rendered from one Catppuccin-styled template.
|
# special-cased, all rendered from one Catppuccin-styled template.
|
||||||
# Useful pages instead of nginx's default 404/502 — see
|
# Useful pages instead of nginx's default 404/502 — see
|
||||||
# `docs/networking/gateway.md::Per-agent error pages` for the design rationale +
|
# `docs/networking/gateway.md::Per-agent error pages` for the design rationale +
|
||||||
# page-vs-status semantics. Consumed by ./vhosts.nix.
|
# page-vs-status semantics. Published as `gateway.lib.errorPages`.
|
||||||
{ pkgs }:
|
{ pkgs }:
|
||||||
let
|
let
|
||||||
mkPage =
|
mkPage =
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
# attrs, and the security headers every vhost in front of this gateway
|
# attrs, and the security headers every vhost in front of this gateway
|
||||||
# needs.
|
# needs.
|
||||||
#
|
#
|
||||||
# Split out of ./vhosts.nix so it has one home and several consumers.
|
# One home, several consumers: ./declared-vhosts.nix (which renders the
|
||||||
# ./vhosts.nix and ./declared-vhosts.nix (which renders the vhosts service
|
# vhosts service modules declare under `gateway.vhosts`) reads it through
|
||||||
# modules declare under `gateway.vhosts`) read it through
|
|
||||||
# `services.hyperhive.gateway.lib` (see ./options.nix) rather than calling
|
# `services.hyperhive.gateway.lib` (see ./options.nix) rather than calling
|
||||||
# this file directly — one source, not a copy that agrees by inspection. A
|
# this file directly — one source, not a copy that agrees by inspection. A
|
||||||
# service module reads `verifiedProxyTo`, `errorPages` and `securityHeaders`
|
# service module reads `verifiedProxyTo`, `errorPages` and `securityHeaders`
|
||||||
|
|
@ -111,9 +110,9 @@ in
|
||||||
# own add_header does NOT inherit the server-level ones. Any
|
# own add_header does NOT inherit the server-level ones. Any
|
||||||
# location with its own add_header (e.g. CORS on /.well-known or
|
# location with its own add_header (e.g. CORS on /.well-known or
|
||||||
# /_matrix/) must repeat the security headers explicitly — see those
|
# /_matrix/) must repeat the security headers explicitly — see those
|
||||||
# locations in ./vhosts.nix. HTML-serving and proxy locations that
|
# locations in ../glue-matrix-hive-discovery.nix and ../hive-matrix.nix.
|
||||||
# carry no add_header of their own pick these up from the server
|
# HTML-serving and proxy locations that carry no add_header of their own
|
||||||
# scope automatically.
|
# pick these up from the server scope automatically.
|
||||||
securityHeaders = ''
|
securityHeaders = ''
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
|
|
||||||
|
|
@ -1,239 +1,14 @@
|
||||||
# The gateway's own vhosts, as `services.hyperhive.gateway.vhosts`
|
# The gateway's own vhost, as a `services.hyperhive.gateway.vhosts`
|
||||||
# declarations (rendered by ./declared-vhosts.nix): the `_` default server,
|
# declaration (rendered by ./declared-vhosts.nix): the `_` default server,
|
||||||
# which answers unmatched `Host` with a bare 444; the dashboard's vhost
|
# which answers unmatched `Host` with a bare 444.
|
||||||
# (`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.
|
|
||||||
#
|
#
|
||||||
# Scope: a swarm service declares its own vhost and dns name in its own
|
# Scope: a service declares its own vhost and dns name in its own module,
|
||||||
# module, the same way. That keeps a service's gateway surface next to
|
# the same way — the hive's dashboard and hive domain in
|
||||||
# the service, and keeps this file to the surface the hive itself serves.
|
# ../hive-c0re/vhosts.nix, matrix discovery in
|
||||||
#
|
# ../glue-matrix-hive-discovery.nix. That keeps a service's gateway surface
|
||||||
# Pure function — called from ./default.nix with the outer-scope config
|
# next to the service, and keeps this file to what the gateway itself
|
||||||
# values as arguments; returns `{ vhosts }`.
|
# 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.
|
|
||||||
#
|
|
||||||
# ⚠️ `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.
|
|
||||||
#
|
|
||||||
# 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`
|
# The catch-all, and now *only* a catch-all: anything whose `Host`
|
||||||
# matches no vhost gets an immediate 444 (close without a response)
|
# matches no vhost gets an immediate 444 (close without a response)
|
||||||
# rather than being served the hive's dashboard.
|
# rather than being served the hive's dashboard.
|
||||||
|
|
@ -263,49 +38,3 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 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;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
# `swarm.dashboardDomain`) — this one answers for the swarm apex and is the
|
# `swarm.dashboardDomain`) — this one answers for the swarm apex and is the
|
||||||
# operator's view across hives.
|
# operator's view across hives.
|
||||||
#
|
#
|
||||||
# Options only. The vhost itself is declared in hive-gateway/vhosts.nix
|
# Options, and the swarm apex's vhost, declared below through
|
||||||
# alongside forge/matrix/authelia: a service module says *how* it is
|
# `gateway.vhosts`: a service module says *how* it is reached, the gateway
|
||||||
# reached, the gateway says *whether this host serves it*.
|
# says *whether this host serves it*.
|
||||||
{
|
{
|
||||||
lib,
|
lib,
|
||||||
config,
|
config,
|
||||||
|
|
@ -252,7 +252,7 @@ in
|
||||||
};
|
};
|
||||||
# Swagger UI: same "nginx hosts the themed dist straight from
|
# Swagger UI: same "nginx hosts the themed dist straight from
|
||||||
# the store, only /api/openapi.json is dynamic" shape as the
|
# the store, only /api/openapi.json is dynamic" shape as the
|
||||||
# per-hive gateway's `swaggerUiLocations`.
|
# hive dashboard's `swaggerUiLocations` (./hive-c0re/vhosts.nix).
|
||||||
"= /api/docs" = {
|
"= /api/docs" = {
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
return 301 /api/docs/;
|
return 301 /api/docs/;
|
||||||
|
|
|
||||||
|
|
@ -484,12 +484,13 @@ let
|
||||||
&& allLocal.systemd.services.hive-c0re.environment.HYPERHIVE_DASHBOARD_DOMAIN == "h1.hives.t.local";
|
&& allLocal.systemd.services.hive-c0re.environment.HYPERHIVE_DASHBOARD_DOMAIN == "h1.hives.t.local";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# The gateway's own three vhosts are declared under `gateway.vhosts` like
|
# The gateway's catch-all and the hive's two vhosts (../host-modules/
|
||||||
# a service's, so the gateway's listen set and issuer choice reach them
|
# hive-c0re/vhosts.nix) are declared under `gateway.vhosts` like a
|
||||||
|
# service's, so the gateway's listen set and issuer choice reach them
|
||||||
# by the same path. The catch-all keeps its bare `return 444;` (no
|
# by the same path. The catch-all keeps its bare `return 444;` (no
|
||||||
# headers: it sends no response) and the dashboard keeps its exact
|
# headers: it sends no response) and the dashboard keeps its exact
|
||||||
# location set.
|
# location set.
|
||||||
name = "the gateway's own vhosts render with its listen set, issuer choice, headers and locations";
|
name = "the catch-all and the hive's vhosts render with the gateway's listen set, issuer choice, headers and locations";
|
||||||
ok =
|
ok =
|
||||||
let
|
let
|
||||||
gw = allLocal.services.hyperhive.gateway;
|
gw = allLocal.services.hyperhive.gateway;
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,31 @@ let
|
||||||
matrixElsewhere.systemd.services.hive-c0re.environment.HIVE_MATRIX_API_URL
|
matrixElsewhere.systemd.services.hive-c0re.environment.HIVE_MATRIX_API_URL
|
||||||
== "https://chat.t.local";
|
== "https://chat.t.local";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# The discovery locations reach the hive domain's vhost from a glue
|
||||||
|
# module, merged into the vhost hive-c0re declares. The matrix-elsewhere
|
||||||
|
# arm reads the same vhost, which still exists there with its redirect,
|
||||||
|
# so its absence of discovery cannot pass on a misspelled name.
|
||||||
|
name = "matrix discovery sits on the hive domain's vhost only where the homeserver runs";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
here = matrixNoBaoIdentity.services.nginx.virtualHosts."h1.t.local".locations;
|
||||||
|
elsewhere = matrixElsewhere.services.nginx.virtualHosts."h1.t.local".locations;
|
||||||
|
in
|
||||||
|
lib.hasInfix ''{"m.homeserver":{"base_url":"https://chat.t.local"}}'' (
|
||||||
|
here."= /.well-known/matrix/client".extraConfig or ""
|
||||||
|
)
|
||||||
|
&& lib.hasInfix ''{"m.server":"chat.t.local:443"}'' (
|
||||||
|
here."= /.well-known/matrix/server".extraConfig or ""
|
||||||
|
)
|
||||||
|
&& lib.hasInfix "rewrite ^/matrix/(.*)$ https://chat.t.local/$1 permanent;" (
|
||||||
|
here."/matrix/".extraConfig or ""
|
||||||
|
)
|
||||||
|
&& lib.hasInfix "return 301 https://h1.hives.t.local$request_uri;" here."/".extraConfig
|
||||||
|
&& elsewhere ? "/"
|
||||||
|
&& !(elsewhere ? "= /.well-known/matrix/client")
|
||||||
|
&& !(elsewhere ? "/matrix/");
|
||||||
|
}
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
runGroup "matrix-core" cases
|
runGroup "matrix-core" cases
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
# the plain vendored dist (./swagger-ui-dist.nix). This is the FULL
|
# the plain vendored dist (./swagger-ui-dist.nix). This is the FULL
|
||||||
# tree hive-gateway's nginx serves at `/api/docs/` — hive-c0re hosts
|
# tree hive-gateway's nginx serves at `/api/docs/` — hive-c0re hosts
|
||||||
# none of it, only the dynamic `/api/openapi.json` (see
|
# none of it, only the dynamic `/api/openapi.json` (see
|
||||||
# nix/host-modules/hive-gateway/vhosts.nix).
|
# nix/host-modules/hive-c0re/vhosts.nix).
|
||||||
#
|
#
|
||||||
# Output layout: every file from `swagger-ui-dist`, with 3 replaced:
|
# Output layout: every file from `swagger-ui-dist`, with 3 replaced:
|
||||||
# index.html adds colors.css (live stylix theme) +
|
# index.html adds colors.css (live stylix theme) +
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@
|
||||||
//! exactly: `#[utoipa::path(...)]` per handler, an `ApiDoc` root, and a
|
//! exactly: `#[utoipa::path(...)]` per handler, an `ApiDoc` root, and a
|
||||||
//! raw JSON route at `/api/openapi.json`. Swagger UI itself is
|
//! raw JSON route at `/api/openapi.json`. Swagger UI itself is
|
||||||
//! nginx-hosted from the nix store, same shape as the per-hive
|
//! nginx-hosted from the nix store, same shape as the per-hive
|
||||||
//! dashboard's (`nix/host-modules/hive-gateway/vhosts.nix`'s
|
//! dashboard's (`nix/host-modules/hive-c0re/vhosts.nix`'s
|
||||||
//! `swarmUiVhost` — a swagger-ui-theme dist under `/api/docs/`, no
|
//! `swaggerUiLocations` — a swagger-ui-theme dist under `/api/docs/`, no
|
||||||
//! fallback to this daemon). Only annotated routes appear in the spec;
|
//! fallback to this daemon). Only annotated routes appear in the spec;
|
||||||
//! an unannotated one just doesn't show up, nothing breaks.
|
//! an unannotated one just doesn't show up, nothing breaks.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue