diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index 2a0e1531..7ce91da8 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -565,6 +565,148 @@ in add_header Referrer-Policy "strict-origin-when-cross-origin" always; ${lib.optionalString cfg.hsts.enable ''add_header Strict-Transport-Security "${hstsDirectives}" always;''} ''; + + # Forge sub-domain vhost. `server_name = forge.domain`, proxies + # all `/` → forgejo. Tuned for git: `client_max_body_size 1G`, + # `proxy_read_timeout 1h` (multi-GB clones). SSH stays direct on + # `forge.sshPort`. See `docs/gateway.md`. Empty attrset when the + # forge isn't behind the gateway. + forgeVhost = lib.optionalAttrs (forgeCfg.enable or false && forgeCfg.behindGateway or false) { + "${forgeCfg.domain}" = vhostTls // { + listen = vhostListen; + extraConfig = securityHeaders; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString forgeCfg.httpPort}/"; + proxyWebsockets = true; + extraConfig = '' + proxy_buffering off; + client_max_body_size 1G; + proxy_read_timeout 1h; + proxy_send_timeout 1h; + ''; + }; + }; + }; + + # Matrix sub-domain vhost. `server_name = matrixCfg.gatewayHost`. + # `/_matrix/*` → tuwunel (CORS *, 50M body cap, 1h long-poll + # timeout). `/` serves fluffychat or 404 if GUI off. nginx + # longer-prefix-wins puts `/_matrix/` ahead of `/`. See + # `docs/gateway.md`. Empty attrset when matrix has no gateway host. + matrixVhost = lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) { + "${matrixCfg.gatewayHost}" = vhostTls // { + listen = vhostListen; + extraConfig = securityHeaders; + locations = { + "/_matrix/" = { + proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}"; + proxyWebsockets = true; + extraConfig = '' + proxy_buffering off; + client_max_body_size 50M; + proxy_read_timeout 1h; + proxy_send_timeout 1h; + ${securityHeaders} + add_header Access-Control-Allow-Origin *; + ''; + }; + } + // lib.optionalAttrs (matrixCfg.gui.enable) ( + { + # fluffychat at sub-domain root, SPA-fallback via + # the Accept-header `$matrix_spa_target` map. + "/" = { + alias = "${matrixCfg.gui.package}/"; + extraConfig = '' + try_files $uri $uri/ $matrix_spa_target =404; + ''; + }; + } + // lib.optionalAttrs (hyperhiveDomain != null) { + # FluffyChat boot-config pre-fill so the client's + # `.well-known/matrix/client` lookup hits the + # right delegation endpoint. + "= /config.json" = { + extraConfig = '' + default_type application/json; + return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}'; + ''; + }; + } + ) + // lib.optionalAttrs (!matrixCfg.gui.enable) { + "/" = { + return = "404"; + }; + }; + }; + }; + + # `_` (default) server location groups, lifted out of the inline + # `//`-chain so the two matrix groups (each with its own `let`) + # read on their own. Composed into the `_` vhost's `locations` + # below alongside the still-inline agent/dashboard/auth groups. + + # `/matrix/*` → 301 → `matrix./$1` (legacy deep-link + # shim during the fluffychat sub-domain move). See `docs/gateway.md`. + matrixRedirectLocations = + lib.optionalAttrs (matrixCfg.enable && matrixCfg.gui.enable && matrixCfg.gatewayHost != null) + ( + let + target = "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}"; + in + { + "/matrix/" = { + extraConfig = '' + rewrite ^/matrix/(.*)$ ${target}/$1 permanent; + ''; + }; + } + ); + + # `.well-known/matrix/{client,server}` discovery JSON. Points + # clients at `matrixCfg.gatewayHost` when set; falls back to direct + # `:`. CORS `*` per matrix spec. The `m.server` + # port-8448 carve-out is documented inline. See `docs/gateway.md`. + wellKnownLocations = lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) ( + let + clientBaseUrl = + if matrixCfg.gatewayHost != null then + "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}" + else + "${publicScheme}://${hyperhiveDomain}:${toString matrixCfg.httpPort}"; + # `m.server` is NOT a URL: per the matrix server-server spec + # (Resolving Server Names) a delegated host with NO port resolves + # to the federation default 8448 (after the SRV check) — the + # https-implies-443 rule does NOT apply here. So the port must be + # explicit even when it's the HTTPS default; `publicPortSuffix` + # (which drops :443) is right for the client base_url above but + # wrong for federation delegation. Without this, peers federate to + # :8448 (closed) while the endpoint actually lives on + # the gateway's 443 vhost. See docs/gateway.md discovery flow. + serverHostPort = + if matrixCfg.gatewayHost != null then + "${matrixCfg.gatewayHost}:${toString publicPort}" + else + "${hyperhiveDomain}:${toString matrixCfg.httpPort}"; + in + { + "= /.well-known/matrix/client" = { + extraConfig = '' + default_type application/json; + ${securityHeaders} + add_header Access-Control-Allow-Origin *; + return 200 '{"m.homeserver":{"base_url":"${clientBaseUrl}"}}'; + ''; + }; + "= /.well-known/matrix/server" = { + extraConfig = '' + default_type application/json; + return 200 '{"m.server":"${serverHostPort}"}'; + ''; + }; + } + ); in { system.stateVersion = "26.05"; @@ -657,14 +799,13 @@ in }; # nginx reload is triggered from the HOST side by hive-c0re - # via `systemctl -M hive-gateway reload nginx` — lets systemd - # resolve the nginx binary path, avoiding exit-203 EXEC failures. - # after each agents.conf write. A path unit watching the - # bind-mounted file inside the container was tried first - # (A path unit inside the container was tried but IN_MOVED_TO from an atomic rename on the host - # does not propagate across the nspawn mount-namespace boundary, - # does not cross the mount-namespace boundary. Host-side trigger is the - # correct approach. + # via `systemctl -M hive-gateway reload nginx` after each + # agents.conf write — letting systemd resolve the nginx binary + # path avoids exit-203 EXEC failures. A path unit watching the + # bind-mounted file inside the container was tried first but + # doesn't work: an IN_MOVED_TO from an atomic rename on the host + # does not propagate across the nspawn mount-namespace boundary. + # The host-side trigger is the correct approach. services.nginx = { enable = true; @@ -687,72 +828,8 @@ in "_" = vhostTls // { listen = vhostListen; locations = - # `/matrix/*` → 301 → `matrix./$1` - # (fluffychat moved to sub-domain root; this - # keeps bookmarks + deep-links working during the - # transition). See `docs/gateway.md` for the vhost - # map. - lib.optionalAttrs (matrixCfg.enable && matrixCfg.gui.enable && matrixCfg.gatewayHost != null) ( - let - target = "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}"; - in - { - "/matrix/" = { - extraConfig = '' - rewrite ^/matrix/(.*)$ ${target}/$1 permanent; - ''; - }; - } - ) - // - # `.well-known/matrix/{client,server}` discovery JSON. - # Points clients at `matrixCfg.gatewayHost` (sub-domain - # vhost) when set; falls back to direct `:` - # when no gateway target. CORS `*` per matrix spec. - # See `docs/gateway.md` "Discovery flow" for the full - # client-bootstrap sequence. - lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) ( - let - clientBaseUrl = - if matrixCfg.gatewayHost != null then - "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}" - else - "${publicScheme}://${hyperhiveDomain}:${toString matrixCfg.httpPort}"; - # `m.server` is NOT a URL: per the matrix - # server-server spec (Resolving Server Names) a - # delegated host with NO port resolves to the - # federation default 8448 (after the SRV check) — - # the https-implies-443 rule does NOT apply here. - # So the port must be explicit even when it's the - # HTTPS default; `publicPortSuffix` (which drops - # :443) is right for the client base_url above but - # wrong for federation delegation. Without this, - # peers federate to :8448 (closed) while - # the endpoint actually lives on the gateway's 443 - # vhost. See docs/gateway.md discovery flow. - serverHostPort = - if matrixCfg.gatewayHost != null then - "${matrixCfg.gatewayHost}:${toString publicPort}" - else - "${hyperhiveDomain}:${toString matrixCfg.httpPort}"; - in - { - "= /.well-known/matrix/client" = { - extraConfig = '' - default_type application/json; - ${securityHeaders} - add_header Access-Control-Allow-Origin *; - return 200 '{"m.homeserver":{"base_url":"${clientBaseUrl}"}}'; - ''; - }; - "= /.well-known/matrix/server" = { - extraConfig = '' - default_type application/json; - return 200 '{"m.server":"${serverHostPort}"}'; - ''; - }; - } - ) + matrixRedirectLocations + // wellKnownLocations // # `/agent/` catch-all: hits when an operator # requests `/agent//...`. Without this the @@ -879,83 +956,8 @@ in ''; }; } - // - # Forge sub-domain vhost. `server_name = - # forge.domain`, proxies all `/` → forgejo. Tuned for - # git: `client_max_body_size 1G`, `proxy_read_timeout 1h` - # (multi-GB clones). SSH stays direct on `forge.sshPort`. - # See `docs/gateway.md`. - lib.optionalAttrs (forgeCfg.enable or false && forgeCfg.behindGateway or false) { - "${forgeCfg.domain}" = vhostTls // { - listen = vhostListen; - extraConfig = securityHeaders; - locations."/" = { - proxyPass = "http://127.0.0.1:${toString forgeCfg.httpPort}/"; - proxyWebsockets = true; - extraConfig = '' - proxy_buffering off; - client_max_body_size 1G; - proxy_read_timeout 1h; - proxy_send_timeout 1h; - ''; - }; - }; - } - // - # Matrix sub-domain vhost. `server_name = - # matrixCfg.gatewayHost`. `/_matrix/*` → tuwunel (CORS *, - # 50M body cap, 1h long-poll timeout). `/` serves - # fluffychat or 404 if GUI off. nginx - # longer-prefix-wins puts `/_matrix/` ahead of `/`. - # See `docs/gateway.md`. - lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) { - "${matrixCfg.gatewayHost}" = vhostTls // { - listen = vhostListen; - extraConfig = securityHeaders; - locations = { - "/_matrix/" = { - proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}"; - proxyWebsockets = true; - extraConfig = '' - proxy_buffering off; - client_max_body_size 50M; - proxy_read_timeout 1h; - proxy_send_timeout 1h; - ${securityHeaders} - add_header Access-Control-Allow-Origin *; - ''; - }; - } - // lib.optionalAttrs (matrixCfg.gui.enable) ( - { - # fluffychat at sub-domain root, SPA-fallback via - # the Accept-header `$matrix_spa_target` map. - "/" = { - alias = "${matrixCfg.gui.package}/"; - extraConfig = '' - try_files $uri $uri/ $matrix_spa_target =404; - ''; - }; - } - // lib.optionalAttrs (hyperhiveDomain != null) { - # FluffyChat boot-config pre-fill so the client's - # `.well-known/matrix/client` lookup hits the - # right delegation endpoint. - "= /config.json" = { - extraConfig = '' - default_type application/json; - return 200 '{"defaultHomeserver":"${hyperhiveDomain}"}'; - ''; - }; - } - ) - // lib.optionalAttrs (!matrixCfg.gui.enable) { - "/" = { - return = "404"; - }; - }; - }; - }; + // forgeVhost + // matrixVhost; }; # Hive-internal DNS resolver, co-located in the