From 5264ef7d5ea97ff7c6f8ccd8cbe72edc8d833a4d Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 4 Jun 2026 12:14:56 +0200 Subject: [PATCH] feat(gateway): add gateway.hsts.enable option, disabled by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HSTS was unconditionally tied to hasTls. This is risky: enabling it on a deployment that later loses TLS locks browsers out until max-age expires. Add three options under services.hyperhive.gateway.hsts: enable — bool, default false maxAge — seconds, default 31536000 (1 year) includeSubDomains — bool, default true HSTS header is now only emitted when hsts.enable = true. --- nix/modules/hive-gateway.nix | 48 +++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index 469262fa..c5b619c4 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -331,6 +331,48 @@ in }; }; + hsts = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Add `Strict-Transport-Security` to all gateway vhosts. + + Disabled by default: HSTS pins HTTPS in the browser's HSTS + preload list; enabling it on a deployment that later loses TLS + will lock browsers out until the max-age expires. Only enable + this when you are certain TLS is permanent. + + Requires TLS to be active (`selfSignedTls = true`, a `tls.certDir`, + or `tls.acme.enable = true`). Enabling HSTS without TLS is + technically harmless (browsers ignore the header over plain HTTP) + but is almost certainly a misconfiguration. + ''; + }; + + maxAge = lib.mkOption { + type = lib.types.ints.positive; + default = 31536000; + example = 86400; + description = '' + Value for the `max-age` directive in seconds. + Default: 31536000 (1 year), which is the value required for + HSTS preload list submission. Use a shorter value (e.g. 86400) + while testing so browsers forget the pin quickly. + ''; + }; + + includeSubDomains = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to include `includeSubDomains` in the HSTS header. + Only disable this if the gateway host has sub-domains that + intentionally serve plain HTTP. + ''; + }; + }; + }; config = lib.mkIf cfg.enable { @@ -499,11 +541,15 @@ in # 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. + hstsDirectives = lib.concatStringsSep "; " ( + [ "max-age=${toString cfg.hsts.maxAge}" ] + ++ lib.optional cfg.hsts.includeSubDomains "includeSubDomains" + ); 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;''} + ${lib.optionalString cfg.hsts.enable ''add_header Strict-Transport-Security "${hstsDirectives}" always;''} ''; in {