diff --git a/docs/gateway.md b/docs/gateway.md index fd86ef1f..66039431 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -1,6 +1,6 @@ # hive-gateway -Single nginx in front of every hyperhive web surface. Runs on the **host**, next to hive-c0re; system-config (not meta-flake managed). Configured via `services.hyperhive.gateway.*` + per-subsystem opt-in flags in `services.hyperhive.{forge,matrix,...}`. (It lived in a `hive-gateway` container until #3088 — one that shared the host netns anyway, so the boundary gave no network isolation while costing a resolv.conf sync, a machine-bus reload and three bind mounts.) +Single nginx in front of every hyperhive web surface. Runs on the **host**, next to hive-c0re, rather than in its own container: it shares the host netns anyway (see [Vhost map](#vhost-map) below), so containerizing it would buy no network isolation while costing a resolv.conf sync, a machine-bus reload, and three bind mounts. System-config (not meta-flake managed). Configured via `services.hyperhive.gateway.*` + per-subsystem opt-in flags in `services.hyperhive.{forge,matrix,...}`. ## Vhost map @@ -21,7 +21,8 @@ The authelia vhost is declared only by the host that **runs** authelia, not by e ⚠️ **A `502` from this vhost usually means authelia has no users yet, not that the proxy is misconfigured.** Authelia treats an empty user store as a fatal startup error, so an enabled-but-unbootstrapped swarm crash-loops the container while the vhost in front of it works perfectly. Check `journalctl -M swarm-authelia -u authelia-swarm` before suspecting anything here; the bootstrap step is in [`swarm/sso.md`](swarm/sso.md). -Per-agent UIs stay sub-path because they're hyperhive-internal and base-path-aware. External standard apps (forge / matrix) get sub-domains because their defaults work cleanly at sub-domain root + per-origin cookies / storage isolation matters. +Per-agent UIs stay sub-path, forge and matrix get sub-domains — see +[Sub-domain shape (rationale)](#sub-domain-shape-rationale) below for why. ## Discovery flow (matrix) @@ -37,7 +38,7 @@ Federation peers fetch `.well-known/matrix/server` → `{"m.server":"matrix.` vhost serve a flutter/SPA bundle via the Accept-header pattern below. (The `` dashboard catch-all used this too but now routes by **path** — see the dashboard note after.) Two requirements collide: +The per-agent UIs and the `matrix.` vhost serve a flutter/SPA bundle via the Accept-header pattern below. The dashboard vhost instead routes by **path** — see [Dashboard: path-based routing](#dashboard-path-based-routing-not-accept-header) below. Two requirements collide: - hard-refresh on a sub-route must serve `index.html` (SPA's client-side router takes over after JS bootstrap) - a non-navigation request that isn't an on-disk asset must NOT get HTML with the wrong content-type @@ -48,13 +49,13 @@ For matrix / per-agent static assets, `` is `=404` (a missing asset is ju ### Dashboard: path-based routing (not Accept-header) -Now that every hive-c0re backend route lives under `/api/` plus the single `/webhook/knowledge` endpoint, the dashboard vhost routes by **path**, not Accept header: +Every hive-c0re backend route lives under `/api/` plus the single `/webhook/knowledge` endpoint, so the dashboard vhost routes by **path**, not Accept header — deterministic, unlike a content-type split where the same URL could resolve differently depending on the caller's `Accept` header: - `location /api/` → hive-c0re (`7000`): all dashboard data, actions/mutations, and the two SSE streams (`/api/dashboard/stream`, `/api/build-logs/id/{id}/stream`). Carries `proxy_buffering off` + a 1d read timeout for the streams. - `location /webhook/` → hive-c0re: the knowledge webhook. - `location /` → the dashboard dist (from the `servedFrontend` nix-store path) with `try_files $uri /index.html` (SPA fallback). -Each location carries a duplicated `auth_basic` block (separate locations don't inherit it). This keeps the gateway static-serving the dashboard dist while hive-c0re stays API-only — a frontend-only change no longer rebuilds + restarts the core daemon. The earlier `map $http_accept` Accept-header split was replaced because it made the *same* URL behave differently by content-type (e.g. `/api/state` fetched with `Accept: text/html` wrongly returned `index.html`); path routing is deterministic. A new top-level c0re route prefix (beyond `/api` + `/webhook`) needs a matching `location` added to the dashboard vhost. +Each location carries a duplicated `auth_basic` block (separate locations don't inherit it). This keeps the gateway static-serving the dashboard dist while hive-c0re stays API-only — a frontend-only change doesn't rebuild or restart the core daemon. A new top-level c0re route prefix (beyond `/api` + `/webhook`) needs a matching `location` added to the dashboard vhost. ## Local dev (`localHostsEntry`) @@ -204,7 +205,13 @@ On by default, and listens on `httpsPort` (default 443) on every vhost beside th The issuer is a **host-held hive CA**, not a bare self-signed leaf. A host service (`hive-tls-ca.service`, from the `hive-tls` module) generates a long-lived CA (`services.hyperhive.tls.caValidityDays`, default ~20y) under `services.hyperhive.tls.stateDir` (default `/var/lib/hive-tls`), then signs a gateway **leaf** (`leafValidityDays`, default 30d) with it. `hive-gateway-self-signed-cert` then imports the leaf into nginx's state dir (`/var/lib/hive-gateway/tls/{cert,key}.pem`). -⚠️ **Do not collapse that import unit into pointing nginx at the CA dir.** It does two jobs. It re-modes the leaf (`hive-tls-ca` writes the key `0600 root:root`; nginx's pre-start `nginx -t` runs as the *nginx user*, so a `0600` key fails the config test and blocks the unit), and it guarantees **every cert path the nginx config names exists** — which is what the swarm-services fallback below is for. Removing it re-creates the #3097 outage. +⚠️ **Do not collapse that import unit into pointing nginx at the CA dir.** +It does two jobs, and skipping it has taken the gateway down in production +before. It re-modes the leaf (`hive-tls-ca` writes the key `0600 +root:root`; nginx's pre-start `nginx -t` runs as the *nginx user*, so a +`0600` key fails the config test and blocks the unit), and it guarantees +**every cert path the nginx config names exists** — which is what the +swarm-services fallback below is for. **Why a CA, not a bare leaf**: a bare self-signed leaf is its own trust anchor, so every regeneration is a new anchor every consumer must re-trust — and a runtime-generated leaf can't be wired into an agent's build-time trust store at all. With a stable CA, agents and federation peers trust it *once*; leaf rotation never re-breaks them. @@ -250,8 +257,8 @@ services.hyperhive.swarm.hives.example = { domain = "example.com"; }; # no cert ### Fronting with an external TLS terminator -There is no http-only mode: the gateway always terminates TLS (self-signed -floor). Two paths for an operator who wants their own TLS terminator: +There is no http-only mode (see [TLS modes](#tls-modes) above). Two paths +for an operator who wants their own TLS terminator: - give the gateway the real cert via `tls.certDir` (or `tls.acme`) so it serves proper TLS directly — no separate proxy needed; or @@ -259,8 +266,8 @@ floor). Two paths for an operator who wants their own TLS terminator: intended direction for "bring your own proxy" — the gateway is not meant to expose an unencrypted TCP upstream). -**`.well-known/matrix/{client,server}` scheme** is always `https` now — the -gateway always terminates TLS, so discovery responses always advertise https. +Because of this, `.well-known/matrix/{client,server}` discovery responses +always advertise `https` (see [Discovery flow](#discovery-flow-matrix) above). ## Firewall posture (host-level) @@ -276,8 +283,8 @@ host-loopback fallback for the pre-UDS/gateway-disabled case, but isn't what the gateway itself proxies through. `services.hyperhive.gateway.openFirewall = true` opens both `port` and -`httpsPort` — the gateway always terminates TLS (self-signed floor), so -both are always served. +`httpsPort` — both are always served, since the gateway always terminates +TLS (see [TLS modes](#tls-modes) above). Every agent hashes into the same port range (no special case), so one range opening covers every container. @@ -589,7 +596,7 @@ header is added alongside the other security headers. enabling it on a deployment that later loses TLS locks browsers out until `max-age` expires. Only enable when TLS is permanent. -The gateway always terminates TLS now (self-signed floor), so HSTS is -always served over https when enabled — the old "HSTS requires a TLS mode" -assertion is gone (it can no longer be violated). +Since the gateway always terminates TLS (see [TLS modes](#tls-modes) +above), an enabled HSTS header is always served over https — there is no +TLS-less mode that could violate it.