From fbea02867ef293bc95d7174a6228b1dbc1be85d6 Mon Sep 17 00:00:00 2001 From: Franz Kafka Date: Thu, 19 Mar 2026 13:42:41 +0000 Subject: [PATCH] feat(nginx): add security headers with per-domain CSP - Add HSTS (6 months, includeSubDomains, preload-ready) - Add X-Content-Type-Options: nosniff - Add Permissions-Policy (disable camera/mic/geolocation) - Add Cross-Origin-Resource-Policy: same-origin - Add Cross-Origin-Opener-Policy: same-origin - Add configurable Content-Security-Policy per domain Per-service CSP tuning: - SearXNG: null (handles its own CSP in settings.yml) - Forgejo: relaxed (unsafe-inline/eval for code highlighting) - Vaultwarden: relaxed (unsafe-eval for WebCrypto vault) Fixes: missing CSP, HSTS, X-Content-Type-Options headers --- configuration.nix | 2 ++ modules/forgejo.nix | 2 ++ modules/nginx.nix | 30 +++++++++++++++++++++++++++++- modules/vaultwarden.nix | 2 ++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/configuration.nix b/configuration.nix index 30039e5..e06794c 100644 --- a/configuration.nix +++ b/configuration.nix @@ -104,6 +104,8 @@ domains = { "search.ashisgreat.xyz" = { port = 8888; + # SearXNG sets its own CSP in settings.yml — omit at Nginx level to avoid conflicts + contentSecurityPolicy = null; }; }; }; diff --git a/modules/forgejo.nix b/modules/forgejo.nix index 23e259c..35ca7f9 100644 --- a/modules/forgejo.nix +++ b/modules/forgejo.nix @@ -100,6 +100,8 @@ in extraConfig = '' client_max_body_size 512M; ''; + # Relaxed CSP for Forgejo — needs inline styles for code highlighting + contentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' wss://${cfg.domain}; frame-ancestors 'self'"; }; # Open SSH port for Git diff --git a/modules/nginx.nix b/modules/nginx.nix index 66fd91d..bce0b6d 100644 --- a/modules/nginx.nix +++ b/modules/nginx.nix @@ -1,5 +1,5 @@ # Nginx Reverse Proxy Module -# Provides: Nginx with automatic Let's Encrypt certificates +# Provides: Nginx with automatic Let's Encrypt certificates and security headers # # Usage: # myModules.nginx = { @@ -39,11 +39,19 @@ in type = lib.types.port; description = "Local port to proxy to"; }; + extraConfig = lib.mkOption { type = lib.types.lines; default = ""; description = "Extra Nginx config for this location"; }; + + contentSecurityPolicy = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'"; + description = "Content-Security-Policy header value. Set to null to omit."; + }; + extraLocations = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule { options = { @@ -96,6 +104,26 @@ in enableACME = true; forceSSL = true; + # Security headers applied per-vhost + extraConfig = '' + # Strict Transport Security — 6 months, include subdomains, preload-ready + add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload" always; + + # Prevent MIME type sniffing + add_header X-Content-Type-Options "nosniff" always; + + # Restrict browser features + add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + + # Cross-origin isolation + add_header Cross-Origin-Resource-Policy "same-origin" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; + + # Content Security Policy (configurable per-domain) + '' + lib.optionalString (opts.contentSecurityPolicy != null) '' + add_header Content-Security-Policy "${opts.contentSecurityPolicy}" always; + ''; + locations = { "/" = { proxyPass = "http://127.0.0.1:${toString opts.port}"; diff --git a/modules/vaultwarden.nix b/modules/vaultwarden.nix index fd393bc..1af04f4 100644 --- a/modules/vaultwarden.nix +++ b/modules/vaultwarden.nix @@ -114,6 +114,8 @@ in extraConfig = '' client_max_body_size 128M; ''; + # Relaxed CSP for Vaultwarden — needs unsafe-eval for WebCrypto vault + contentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://haveibeenpwned.com; font-src 'self'; connect-src 'self' wss://${cfg.domain} https://api.bitwarden.com https://haveibeenpwned.com; frame-ancestors 'self'"; extraLocations."/notifications/hub" = { proxyPass = "http://127.0.0.1:${toString cfg.websocketPort}"; extraConfig = '' -- 2.53.0