diff --git a/docs/gateway.md b/docs/gateway.md index 5f1d5e25..edc372fa 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -21,13 +21,11 @@ Per-agent UIs stay sub-path because they're hyperhive-internal and base-path-awa 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). With `selfSignedTls = false` the scheme drops to http and the port suffix reflects the bare `port` instead. +1. Client fetches `http:///.well-known/matrix/client` → `{"m.homeserver":{"base_url":"http://matrix."}}` (no port suffix when gateway listens on 80). 2. Client connects to `matrix./_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":"matrix."}` and connect to `matrix.:8448` per spec default. Gateway only listens on configured `port`; cross-hive federation needs either an SRV record (`_matrix._tcp.matrix.` → port 80) 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) @@ -133,22 +131,6 @@ frontend-side derivation. Next-up tracked separately: #14 (container netns isolation), TLS (#594). -## Self-signed TLS (`selfSignedTls`) - -On by default since #837. The gateway generates a self-signed RSA-4096 cert at first boot (10-year validity) and listens on `httpsPort` (default 443) on every vhost beside the plain-http `port` (default 80). - -**Why on by default**: matrix-dart-sdk (FluffyChat's SDK) hardcodes `https:///.well-known/matrix/client` for homeserver discovery and refuses to fall back to plain http. Without TLS the browser client cannot bootstrap (#837). For headless agent traffic and operator dashboard reach plain http is fine, so the http listen stays in parallel — operators can keep using `http:///` from the dashboard if they don't care about the cert prompt. - -**Cert shape**: subject CN = bare hive domain; subjectAltName covers `` + wildcard `*.` so all current and future sub-domain vhosts (matrix, forge, ...) validate under the same cert. Stored at `/var/lib/hive-gateway/tls/{cert,key}.pem` inside the gateway container (`ephemeral = false`, so persisted across container restart). - -**Regeneration**: the generator unit (`hive-gateway-self-signed-cert.service`) is gated by `ConditionPathExists=!cert.pem`, so it's a one-shot. To rotate (e.g. cert leak, expiry approaching), delete `cert.pem` inside the gateway container and restart `nginx.service` — the generator runs as a `Before=` dependency. No automatic rotation; the cert is purely a workaround for the well-known fetch. - -**Production**: operators fronting hyperhive with a real reverse proxy (caddy + ACME, traefik + Let's Encrypt, etc.) set `selfSignedTls = false`. The proxy handles termination upstream, the gateway listens on `port` only. - -**Cert prompts**: browsers warn once per host on first visit. With the wildcard SAN, `https:///`, `https://matrix./`, and `https://forge./` are covered by the same cert, but the browser still prompts per origin (per-host security state). FluffyChat needs the user to accept both `matrix.` (the SPA itself) and `` (the well-known fetch endpoint). - -**`.well-known/matrix/{client,server}` scheme**: switches to `https` when `selfSignedTls` is on, so matrix-dart-sdk doesn't downgrade and tuwunel federation peers get a TLS-fronted base_url. The legacy `/matrix/*` 301 redirect also flips to https. - ## Firewall posture (host-level) `hive-c0re.nix` opens the per-agent web-port range @@ -159,11 +141,6 @@ On by default since #837. The gateway generates a self-signed RSA-4096 cert at f firewall-open would defeat the single-front-door story (closes #621). -`services.hyperhive.gateway.openFirewall = true` opens `port` plus -`httpsPort` when `selfSignedTls = true` (default). Operators who -flip `selfSignedTls = false` to front the gateway with a real -TLS-terminating reverse proxy on the host get only `port` opened. - Manager hashes into the same range since #753 (no more "manager pinned at 8000" special case), so one range opening covers every container. diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index 61b4cd18..df43b867 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -197,46 +197,6 @@ in ''; }; - selfSignedTls = lib.mkOption { - type = lib.types.bool; - default = true; - example = false; - description = '' - Generate a self-signed TLS cert at first gateway boot and - listen on `httpsPort` (default 443) with it on every vhost. - On by default because matrix-dart-sdk (the SDK behind - FluffyChat + several other Matrix clients) hardcodes - `https:///.well-known/matrix/client` for homeserver - discovery and refuses to fall back to plain http — without - TLS the browser client just won't connect (#837). - - Self-signed means browsers will show a "not secure" warning - on first visit; the operator clicks through once per - browser. For production deployments, set this to `false` - and front the gateway with a reverse proxy (caddy, traefik, - or nginx with ACME) that does proper TLS termination. - - The cert is regenerated on demand if the file is missing - but never rotated automatically; delete - `/var/lib/hive-gateway/tls/cert.pem` inside the gateway - container to force a fresh one. - - See `docs/gateway.md` ("Self-signed TLS"). - ''; - }; - - httpsPort = lib.mkOption { - type = lib.types.port; - default = 443; - example = 8443; - description = '' - TCP port for the TLS-terminated vhosts when `selfSignedTls` - is enabled. Default 443. Setting `selfSignedTls = false` - renders this option inert (the gateway listens on `port` - only). - ''; - }; - agentPortsFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = "/var/lib/hyperhive/agent-ports.json"; @@ -351,101 +311,8 @@ in }; config = { pkgs, ... }: - let - tlsDir = "/var/lib/hive-gateway/tls"; - tlsCert = "${tlsDir}/cert.pem"; - tlsKey = "${tlsDir}/key.pem"; - # Listen addresses every vhost shares. Plain http on `cfg.port` - # always; `cfg.httpsPort` with TLS sits beside it when - # `cfg.selfSignedTls` is on. See `docs/gateway.md` - # ("Self-signed TLS") for the cert lifecycle. - vhostListen = [ - { - addr = "0.0.0.0"; - port = cfg.port; - } - ] - ++ lib.optional cfg.selfSignedTls { - addr = "0.0.0.0"; - port = cfg.httpsPort; - ssl = true; - }; - # nixos `services.nginx.virtualHosts.` ssl attrs to mix - # into each vhost when self-signed TLS is on. `addSSL = true` - # is what gates `ssl_certificate` directive emission in the - # nixos nginx module (`hasSSL` checks addSSL / onlySSL / - # forceSSL). The actual ssl listen is the explicit entry - # with `ssl = true` in `vhostListen` — the nixos module's - # auto-listen-generation only kicks in when `listen` is - # empty, so the explicit listen wins and there's no - # duplicate-listen risk. Empty otherwise so the http-only - # path stays identical. - vhostTls = lib.optionalAttrs cfg.selfSignedTls { - addSSL = true; - sslCertificate = tlsCert; - sslCertificateKey = tlsKey; - }; - - # Public-facing scheme + port-suffix for URLs the gateway - # mints into responses (well-known JSON, the deprecated - # `/matrix/*` 301 redirect, future absolute-URL needs). - # When self-signed TLS is on, prefer `https://` (matrix- - # spec compliance) — 443 elides the port. Otherwise fall back - # to the plain-http listen with the bare port. See - # `docs/gateway.md` ("Self-signed TLS"). Shared at this scope - # (was inlined twice, argus 🟡 on #848). - publicScheme = if cfg.selfSignedTls then "https" else "http"; - publicPort = if cfg.selfSignedTls then cfg.httpsPort else cfg.port; - publicPortDefault = if cfg.selfSignedTls then 443 else 80; - publicPortSuffix = - if publicPort == publicPortDefault then "" else ":${toString publicPort}"; - in { system.stateVersion = "26.05"; - - # Generate a self-signed cert on first boot if missing. nginx - # waits on this (Before=) so we never start with a broken - # ssl_certificate path. Cert covers the bare hive domain plus - # `*.${hyperhiveDomain}` so the matrix + forge sub-domains - # are valid under the same cert. See `docs/gateway.md` - # ("Self-signed TLS"). - systemd.services.hive-gateway-self-signed-cert = lib.mkIf cfg.selfSignedTls { - description = "Generate self-signed TLS cert for hive-gateway"; - wantedBy = [ "multi-user.target" ]; - before = [ "nginx.service" ]; - unitConfig.ConditionPathExists = "!${tlsCert}"; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - UMask = "0077"; - }; - path = [ - pkgs.openssl - pkgs.coreutils - ]; - script = - let - subjectCN = if hyperhiveDomain != null then hyperhiveDomain else "hyperhive.local"; - # `subjectAltName` covers the bare hive + the canonical - # sub-domains. Includes a wildcard `*.${domain}` so any - # future sub-domain we mint (per-agent UI under a - # sub-domain, etc.) inherits the cert without a rebuild. - sanLines = lib.concatStringsSep "," ( - [ "DNS:${subjectCN}" ] ++ lib.optional (hyperhiveDomain != null) "DNS:*.${hyperhiveDomain}" - ); - in - '' - mkdir -p ${tlsDir} - openssl req -x509 -newkey rsa:4096 -nodes -sha256 -days 3650 \ - -keyout ${tlsKey} \ - -out ${tlsCert} \ - -subj "/CN=${subjectCN}" \ - -addext "subjectAltName=${sanLines}" - chmod 0600 ${tlsKey} - chmod 0644 ${tlsCert} - ''; - }; - services.nginx = { enable = true; recommendedProxySettings = true; @@ -462,8 +329,13 @@ in } ''; virtualHosts = { - "_" = vhostTls // { - listen = vhostListen; + "_" = { + listen = [ + { + addr = "0.0.0.0"; + port = cfg.port; + } + ]; locations = # `/matrix/*` → 301 → `matrix./$1` # (fluffychat moved to sub-domain root in #772; this @@ -472,7 +344,8 @@ in # map. lib.optionalAttrs (matrixCfg.enable && matrixCfg.gui.enable && matrixCfg.gatewayHost != null) ( let - target = "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}"; + portSuffix = if cfg.port == 80 then "" else ":${toString cfg.port}"; + target = "http://${matrixCfg.gatewayHost}${portSuffix}"; in { "/matrix/" = { @@ -491,14 +364,15 @@ in # client-bootstrap sequence. lib.optionalAttrs (matrixCfg.enable && hyperhiveDomain != null) ( let + portSuffix = if cfg.port == 80 then "" else ":${toString cfg.port}"; clientBaseUrl = if matrixCfg.gatewayHost != null then - "${publicScheme}://${matrixCfg.gatewayHost}${publicPortSuffix}" + "http://${matrixCfg.gatewayHost}${portSuffix}" else - "${publicScheme}://${hyperhiveDomain}:${toString matrixCfg.httpPort}"; + "http://${hyperhiveDomain}:${toString matrixCfg.httpPort}"; serverHostPort = if matrixCfg.gatewayHost != null then - "${matrixCfg.gatewayHost}${publicPortSuffix}" + "${matrixCfg.gatewayHost}${portSuffix}" else "${hyperhiveDomain}:${toString matrixCfg.httpPort}"; in @@ -600,8 +474,13 @@ in # (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; + "${forgeCfg.domain}" = { + listen = [ + { + addr = "0.0.0.0"; + port = cfg.port; + } + ]; locations."/" = { proxyPass = "http://127.0.0.1:${toString forgeCfg.httpPort}/"; proxyWebsockets = true; @@ -622,8 +501,13 @@ in # longer-prefix-wins puts `/_matrix/` ahead of `/`. # See `docs/gateway.md`. lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) { - "${matrixCfg.gatewayHost}" = vhostTls // { - listen = vhostListen; + "${matrixCfg.gatewayHost}" = { + listen = [ + { + addr = "0.0.0.0"; + port = cfg.port; + } + ]; locations = { "/_matrix/" = { proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}"; @@ -729,7 +613,7 @@ in }; networking.firewall = lib.mkIf cfg.openFirewall { - allowedTCPPorts = [ cfg.port ] ++ lib.optional cfg.selfSignedTls cfg.httpsPort; + allowedTCPPorts = [ cfg.port ]; }; # `/etc/hosts` entries for local dev — bare hive domain + any