From 0e84e72c21b0b067b87cc9d1c26f8e6758660f1e Mon Sep 17 00:00:00 2001 From: iris Date: Sat, 15 Aug 2026 12:24:35 +0200 Subject: [PATCH] docs(gateway): fix stale gateway.enable, selfSignedTls, rootUrl, vhost claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified against nix/host-modules/hive-gateway/*.nix + hive-matrix.nix + hive-forge/default.nix: - services.hyperhive.gateway.enable was removed (gateway is now unconditional) — fixes the Dashboard link shape + Firewall posture sections, which described a gateway-off mode that no longer exists. - gateway.selfSignedTls is a removed option (fails eval), not a deprecated-and-ignored one. - The forge rootUrl auto-derivation now uses https:// behind the gateway (httpsPort), not http://. - Vhost map + discovery-flow + local-dev sections used matrix. / forge.; current defaults are chat. and forge. (both swarm-domain-rooted, per hive-matrix.nix's gatewayHost and hive-forge's domain option). - hive-c0re.nix is now a directory (nix/host-modules/hive-c0re/); pointed the HIVE_FORGE_URL reference at the actual file, environment.nix. --- docs/gateway.md | 79 +++++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/docs/gateway.md b/docs/gateway.md index 66039431..0bbbf6a8 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -9,11 +9,11 @@ Single nginx in front of every hyperhive web surface. Runs on the **host**, next | `/` | `_` (catch-all) | dashboard dist (static, from `servedFrontend`); `/api/` + `/webhook/` → hive-c0re (`7000`) | always | | `/agent//` | `_` | per-agent harness (UDS or TCP) | `agents.conf` (runtime-generated) | | `/.well-known/matrix/{client,server}` | `_` | inline JSON (no upstream) | `matrix.enable && domain != null` | -| `/matrix/` (deprecated) | `_` | 301 → `matrix./` | `matrix.gui.enable` | -| `forge./` | `forge.` | forgejo (`3000`) | `forge.behindGateway` | -| `matrix./_matrix/*` | `matrix.` | tuwunel (`8008`) | `matrix.gatewayHost != null` | -| `matrix./` | `matrix.` | fluffychat-web static | `matrix.gui.enable` | -| `matrix./config.json` | `matrix.` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` | +| `/matrix/` (deprecated) | `_` | 301 → `chat./` | `matrix.gui.enable` | +| `forge./` | `forge.` | forgejo (`3000`) | `forge.behindGateway` | +| `chat./_matrix/*` | `chat.` | tuwunel (`8008`) | `matrix.gatewayHost != null` | +| `chat./` | `chat.` | fluffychat-web static | `matrix.gui.enable` | +| `chat./config.json` | `chat.` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` | | `auth./` | `auth.` | authelia (`9091`) | `swarm.authelia.enable` | | `/` | `` | swarm-ui dist (static), behind an authelia subrequest | `swarm.ui.enable` | @@ -28,17 +28,17 @@ Per-agent UIs stay sub-path, forge and matrix get sub-domains — see Operator points client at ``. Sequence: -1. Client fetches `https:///.well-known/matrix/client` → `{"m.homeserver":{"base_url":"https://matrix."}}` (no port suffix when gateway listens on 443). The gateway always terminates TLS, so the scheme is always `https`; a non-default `httpsPort` is reflected as the port suffix. -2. Client connects to `matrix./_matrix/client/...`. +1. Client fetches `https:///.well-known/matrix/client` → `{"m.homeserver":{"base_url":"https://chat."}}` (no port suffix when gateway listens on 443). The gateway always terminates TLS, so the scheme is always `https`; a non-default `httpsPort` is reflected as the port suffix. +2. Client connects to `chat./_matrix/client/...`. 3. Gateway routes `/_matrix/*` → tuwunel at `127.0.0.1:8008`. matrix-dart-sdk (FluffyChat etc.) hardcodes `https` for the well-known fetch regardless of input scheme, so the discovery endpoint MUST be https — see "Self-signed TLS" below for the cert generation that backs the default-on path. -Federation peers fetch `.well-known/matrix/server` → `{"m.server":"matrix."}` and connect to `matrix.:8448` per spec default. Gateway only listens on configured `port` (+ `httpsPort` when TLS on); cross-hive federation needs either an SRV record (`_matrix._tcp.matrix.` → port 80 / 443) OR `matrix.openFirewall = true` so peers reach tuwunel's federation port directly. Hyperhive is mostly closed/internal, so this rarely bites. +Federation peers fetch `.well-known/matrix/server` → `{"m.server":"chat.:"}` (the federation delegation always carries an explicit port, even the HTTPS default 443 — the https-implies-443 elision only applies to the client base_url above). Gateway only listens on configured `port` (+ `httpsPort` when TLS on); cross-hive federation needs either an SRV record (`_matrix._tcp.chat.` → port 80 / 443) OR `matrix.openFirewall = true` so peers reach tuwunel's federation port directly. Hyperhive is mostly closed/internal, so this rarely bites. ## SPA fallback (Accept-header pattern) -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: +The per-agent UIs and the `chat.` 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 @@ -62,8 +62,8 @@ Each location carries a duplicated `auth_basic` block (separate locations don't `services.hyperhive.gateway.localHostsEntry = true` adds entries to the host's `/etc/hosts`: - `` → `127.0.0.1` -- `forge.` → `127.0.0.1` (when forge.behindGateway) -- `matrix.` → `127.0.0.1` (when matrix.gatewayHost set) +- `forge.` → `127.0.0.1` (when forge.behindGateway) +- `chat.` → `127.0.0.1` (when matrix.gatewayHost set) - `auth.` → `127.0.0.1` (when swarm.authelia.enable) `lib.unique` de-dupes if any sub-domain happens to equal another entry. Operators with real DNS leave it off. @@ -150,8 +150,11 @@ When the gateway is in front, the SW4RM tab builds per-agent links as same-origin `/agent//…` URLs instead of the legacy direct `http://:/` TCP shape. The signal comes from `StateSnapshot.gateway_enabled`, sourced from the -`HIVE_GATEWAY_ENABLED` env the c0re NixOS module sets when -`services.hyperhive.gateway.enable = true`. Three render sites +`HIVE_GATEWAY_ENABLED` env the c0re NixOS module now always sets +(`services.hyperhive.gateway.enable` was removed — the gateway runs +unconditionally alongside hyperhive), so this is effectively always +true; the `false` branch is retained as a defensive fallback for the +env being unset. Three render sites flip together: the primary agent-name link, the favicon fetch (`/icon`), and the nav-strip `container`-kind links from `DashboardState.links` (`GET /api/dashboard-state`). `forge`-kind nav-strip links still @@ -172,10 +175,10 @@ selected by which (if any) external TLS source is set: | ACME (Let's Encrypt) | `tls.acme.enable = true` | nginx via HTTP-01 | `https` | | operator cert | `tls.certDir` set | read from the operator's dir | `https` | -The `gateway.selfSignedTls` option is **deprecated and ignored** — self-signed -is now derived from the absence of `tls.certDir` / `tls.acme`. Setting it to -`false` (which used to select http-only or force an external cert) warns and -has no effect; use `tls.certDir` / `tls.acme` to override the default. +The `gateway.selfSignedTls` option has been **removed** — self-signed +is now derived from the absence of `tls.certDir` / `tls.acme`. A config +that still sets it fails eval with a removal message; use `tls.certDir` +/ `tls.acme` to override the default. ### ACME / Let's Encrypt (`tls.acme`) @@ -271,16 +274,17 @@ always advertise `https` (see [Discovery flow](#discovery-flow-matrix) above). ## Firewall posture (host-level) -`hive-c0re.nix` opens the per-agent web-port range -`8100..8999` in the host firewall **only when -`services.hyperhive.gateway.enable = false`**. With the gateway on -(default) it's the sole external entry point and routes to agents over -the UDS upstream described above (see [Per-agent unix-socket -upstream](#per-agent-unix-socket-upstream)) — leaving the per-agent -ports firewall-open would defeat the single-front-door story. The -hashed TCP port (`lifecycle::agent_web_port`) still exists as a direct -host-loopback fallback for the pre-UDS/gateway-disabled case, but isn't -what the gateway itself proxies through. +The gateway is unconditional — `services.hyperhive.gateway.enable` was +removed, there is no gateway-off mode. nginx is always the sole +external entry point and routes to agents over the UDS upstream +described above (see [Per-agent unix-socket +upstream](#per-agent-unix-socket-upstream)), so the per-agent web-port +range `8100..8999` stays closed on the host firewall +unconditionally — opening it would defeat the single-front-door story. +The hashed TCP port (`lifecycle::agent_web_port`) still exists as a +fallback bind for an agent whose `HIVE_WEB_SOCKET` env somehow ends up +unset, but nothing opens a matching firewall hole for it and the +gateway itself never proxies through it. `services.hyperhive.gateway.openFirewall = true` opens both `port` and `httpsPort` — both are always served, since the gateway always terminates @@ -302,9 +306,10 @@ proxy in front. Agents poll `HIVE_FORGE_URL` for Forgejo notifications + run all `hive-forge` calls against it. Network isolation is always on (the shared-netns mode was removed), so agents run in a private netns and -can never reach the host's loopback. `hive-c0re.nix` sets -`HIVE_FORGE_URL` to `http://` (default -`forge.`; `services.hyperhive.domain` is required). Agents +can never reach the host's loopback. +`nix/host-modules/hive-c0re/environment.nix` sets `HIVE_FORGE_URL` to +`http://` (default `forge.` — a swarm runs +one forge; `services.hyperhive.domain` is required). Agents get the bridge dnsmasq as their resolver, resolve the hostname → bridge IP, then reach nginx on port 80 (the bridge firewall opens 80+443). nginx proxies to forgejo — the same path an operator browser @@ -376,14 +381,16 @@ covers most cases: | Shape | Auto-derived `ROOT_URL` | |---|---| -| `behindGateway = true` | `http:///` (port suffix omitted when `gateway.port == 80`) | +| `behindGateway = true` | `https:///` (port suffix omitted when `gateway.httpsPort == 443`) | | `behindGateway = false` | `http://:/` | -The auto-derivation always uses `http://`. Set `rootUrl` explicitly when -you need `https://` (e.g. behind a TLS-terminating reverse proxy, or when -clone URLs must carry `https://` because the gateway terminates TLS), or -when `forge.domain` resolves differently from the public URL. Must end with -`/` (Forgejo requirement; an assertion enforces this). +The gateway always terminates TLS, so the `behindGateway = true` case is +always advertised over `https://`; only the direct (`behindGateway = +false`) shape stays `http://`. Set `rootUrl` explicitly when +`forge.domain` resolves differently from the public URL, or for a +genuinely bespoke shape (e.g. an external reverse proxy on a different +host/path). Must end with `/` (Forgejo requirement; an assertion +enforces this). ## Per-agent static frontend split