Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ec1306c7b | ||
|
|
5264ef7d5e | ||
|
|
18941848dc |
1 changed files with 80 additions and 13 deletions
|
|
@ -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 {
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
@ -375,6 +417,18 @@ in
|
||||||
Let's Encrypt needs a contact address for the ACME account.
|
Let's Encrypt needs a contact address for the ACME account.
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
assertion =
|
||||||
|
!cfg.hsts.enable
|
||||||
|
|| cfg.selfSignedTls
|
||||||
|
|| cfg.tls.certDir != null
|
||||||
|
|| cfg.tls.acme.enable;
|
||||||
|
message = ''
|
||||||
|
services.hyperhive.gateway.hsts.enable = true requires TLS to be
|
||||||
|
configured (selfSignedTls, tls.certDir, or tls.acme.enable). HSTS
|
||||||
|
over plain HTTP is ignored by browsers and indicates a config error.
|
||||||
|
'';
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
# Ensure bind-mount sources exist at host boot before the gateway
|
# Ensure bind-mount sources exist at host boot before the gateway
|
||||||
|
|
@ -444,15 +498,9 @@ in
|
||||||
# - selfSignedTls=true: generated cert stored in persistent state dir.
|
# - selfSignedTls=true: generated cert stored in persistent state dir.
|
||||||
# - tls.certDir set: operator-provided cert bind-mounted at /run/hive-tls.
|
# - tls.certDir set: operator-provided cert bind-mounted at /run/hive-tls.
|
||||||
tlsCert =
|
tlsCert =
|
||||||
if cfg.tls.certDir != null then
|
if cfg.tls.certDir != null then "/run/hive-tls/${cfg.tls.certName}" else "${tlsDir}/cert.pem";
|
||||||
"/run/hive-tls/${cfg.tls.certName}"
|
|
||||||
else
|
|
||||||
"${tlsDir}/cert.pem";
|
|
||||||
tlsKey =
|
tlsKey =
|
||||||
if cfg.tls.certDir != null then
|
if cfg.tls.certDir != null then "/run/hive-tls/${cfg.tls.keyName}" else "${tlsDir}/key.pem";
|
||||||
"/run/hive-tls/${cfg.tls.keyName}"
|
|
||||||
else
|
|
||||||
"${tlsDir}/key.pem";
|
|
||||||
# True when nginx should listen with TLS (any mode).
|
# True when nginx should listen with TLS (any mode).
|
||||||
hasTls = cfg.selfSignedTls || cfg.tls.certDir != null || cfg.tls.acme.enable;
|
hasTls = cfg.selfSignedTls || cfg.tls.certDir != null || cfg.tls.acme.enable;
|
||||||
# Listen addresses every vhost shares. Plain http on `cfg.port`
|
# Listen addresses every vhost shares. Plain http on `cfg.port`
|
||||||
|
|
@ -498,6 +546,23 @@ in
|
||||||
publicPort = if hasTls then cfg.httpsPort else cfg.port;
|
publicPort = if hasTls then cfg.httpsPort else cfg.port;
|
||||||
publicPortDefault = if hasTls then 443 else 80;
|
publicPortDefault = if hasTls then 443 else 80;
|
||||||
publicPortSuffix = if publicPort == publicPortDefault then "" else ":${toString publicPort}";
|
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.
|
||||||
|
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 cfg.hsts.enable ''add_header Strict-Transport-Security "${hstsDirectives}" always;''}
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
system.stateVersion = "26.05";
|
system.stateVersion = "26.05";
|
||||||
|
|
@ -794,7 +859,7 @@ in
|
||||||
# agents without a nixos-rebuild. nginx's longest-prefix-
|
# agents without a nixos-rebuild. nginx's longest-prefix-
|
||||||
# match rule ensures `/agent/<name>/` from this file beats
|
# match rule ensures `/agent/<name>/` from this file beats
|
||||||
# the `/agent/` catch-all above.
|
# the `/agent/` catch-all above.
|
||||||
extraConfig = ''
|
extraConfig = securityHeaders + ''
|
||||||
include /run/hive-state/agents.conf;
|
include /run/hive-state/agents.conf;
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
@ -808,6 +873,7 @@ in
|
||||||
lib.optionalAttrs (forgeCfg.enable or false && forgeCfg.behindGateway or false) {
|
lib.optionalAttrs (forgeCfg.enable or false && forgeCfg.behindGateway or false) {
|
||||||
"${forgeCfg.domain}" = vhostTls // {
|
"${forgeCfg.domain}" = vhostTls // {
|
||||||
listen = vhostListen;
|
listen = vhostListen;
|
||||||
|
extraConfig = securityHeaders;
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://127.0.0.1:${toString forgeCfg.httpPort}/";
|
proxyPass = "http://127.0.0.1:${toString forgeCfg.httpPort}/";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
|
|
@ -830,6 +896,7 @@ in
|
||||||
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) {
|
lib.optionalAttrs (matrixCfg.enable && matrixCfg.gatewayHost != null) {
|
||||||
"${matrixCfg.gatewayHost}" = vhostTls // {
|
"${matrixCfg.gatewayHost}" = vhostTls // {
|
||||||
listen = vhostListen;
|
listen = vhostListen;
|
||||||
|
extraConfig = securityHeaders;
|
||||||
locations = {
|
locations = {
|
||||||
"/_matrix/" = {
|
"/_matrix/" = {
|
||||||
proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}";
|
proxyPass = "http://127.0.0.1:${toString matrixCfg.httpPort}";
|
||||||
|
|
@ -935,10 +1002,10 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall = lib.mkIf cfg.openFirewall {
|
networking.firewall = lib.mkIf cfg.openFirewall {
|
||||||
allowedTCPPorts =
|
allowedTCPPorts = [
|
||||||
[ cfg.port ]
|
cfg.port
|
||||||
++ lib.optional (cfg.selfSignedTls || cfg.tls.certDir != null || cfg.tls.acme.enable)
|
]
|
||||||
cfg.httpsPort;
|
++ lib.optional (cfg.selfSignedTls || cfg.tls.certDir != null || cfg.tls.acme.enable) cfg.httpsPort;
|
||||||
};
|
};
|
||||||
|
|
||||||
# `/etc/hosts` entries for local dev — bare hive domain + any
|
# `/etc/hosts` entries for local dev — bare hive domain + any
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue