diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index 7ce91da8..2a0e1531 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -565,148 +565,6 @@ 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"; @@ -799,13 +657,14 @@ in }; # nginx reload is triggered from the HOST side by hive-c0re - # 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. + # 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. services.nginx = { enable = true; @@ -828,8 +687,72 @@ in "_" = vhostTls // { listen = vhostListen; locations = - matrixRedirectLocations - // wellKnownLocations + # `/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}"}'; + ''; + }; + } + ) // # `/agent/` catch-all: hits when an operator # requests `/agent//...`. Without this the @@ -956,8 +879,83 @@ in ''; }; } - // forgeVhost - // matrixVhost; + // + # 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"; + }; + }; + }; + }; }; # Hive-internal DNS resolver, co-located in the