feat(gateway): add gateway.hsts.enable option, disabled by default

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.
This commit is contained in:
atlas 2026-06-04 12:14:56 +02:00 committed by mara
commit 5264ef7d5e

View file

@ -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
{