diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index b5065a90..469262fa 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -444,15 +444,9 @@ in # - selfSignedTls=true: generated cert stored in persistent state dir. # - tls.certDir set: operator-provided cert bind-mounted at /run/hive-tls. tlsCert = - if cfg.tls.certDir != null then - "/run/hive-tls/${cfg.tls.certName}" - else - "${tlsDir}/cert.pem"; + if cfg.tls.certDir != null then "/run/hive-tls/${cfg.tls.certName}" else "${tlsDir}/cert.pem"; tlsKey = - if cfg.tls.certDir != null then - "/run/hive-tls/${cfg.tls.keyName}" - else - "${tlsDir}/key.pem"; + if cfg.tls.certDir != null then "/run/hive-tls/${cfg.tls.keyName}" else "${tlsDir}/key.pem"; # True when nginx should listen with TLS (any mode). hasTls = cfg.selfSignedTls || cfg.tls.certDir != null || cfg.tls.acme.enable; # Listen addresses every vhost shares. Plain http on `cfg.port` @@ -498,6 +492,19 @@ in publicPort = if hasTls then cfg.httpsPort else cfg.port; publicPortDefault = if hasTls then 443 else 80; publicPortSuffix = if publicPort == publicPortDefault then "" else ":${toString publicPort}"; + + # Security headers added at the server scope on every vhost. + # nginx's add_header inheritance rule: a location that defines its + # own add_header does NOT inherit the server-level ones, so API + # locations with CORS headers (e.g. /.well-known/matrix/client, + # /_matrix/) are unaffected. HTML-serving and proxy locations that + # carry no add_header of their own pick these up automatically. + securityHeaders = '' + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + ${lib.optionalString hasTls ''add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;''} + ''; in { system.stateVersion = "26.05"; @@ -794,7 +801,7 @@ in # agents without a nixos-rebuild. nginx's longest-prefix- # match rule ensures `/agent//` from this file beats # the `/agent/` catch-all above. - extraConfig = '' + extraConfig = securityHeaders + '' include /run/hive-state/agents.conf; ''; }; @@ -808,6 +815,7 @@ in 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; @@ -830,6 +838,7 @@ in 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}"; @@ -935,10 +944,10 @@ in }; networking.firewall = lib.mkIf cfg.openFirewall { - allowedTCPPorts = - [ cfg.port ] - ++ lib.optional (cfg.selfSignedTls || cfg.tls.certDir != null || cfg.tls.acme.enable) - cfg.httpsPort; + allowedTCPPorts = [ + cfg.port + ] + ++ lib.optional (cfg.selfSignedTls || cfg.tls.certDir != null || cfg.tls.acme.enable) cfg.httpsPort; }; # `/etc/hosts` entries for local dev — bare hive domain + any