diff --git a/configuration.nix b/configuration.nix index 4fcd4a4..a7e1c31 100644 --- a/configuration.nix +++ b/configuration.nix @@ -101,11 +101,19 @@ myModules.nginx = { enable = true; email = "info@ashisgreat.xyz"; + rateLimit = { + enable = true; + requests = 10; + burst = 20; + }; domains = { "search.ashisgreat.xyz" = { port = 8888; # SearXNG sets its own CSP in settings.yml — omit at Nginx level to avoid conflicts contentSecurityPolicy = null; + # Search engine — slightly more permissive for bot traffic + rateLimit.requests = 20; + rateLimit.burst = 40; }; }; }; diff --git a/modules/nginx.nix b/modules/nginx.nix index bce0b6d..190d0ed 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 and security headers +# Provides: Nginx with automatic Let's Encrypt certificates, security headers, and rate limiting # # Usage: # myModules.nginx = { @@ -32,6 +32,28 @@ in description = "Email address for Let's Encrypt registration"; }; + rateLimit = { + enable = lib.mkEnableOption "Nginx rate limiting"; + + zone = lib.mkOption { + type = lib.types.str; + default = "10m"; + description = "Size of the shared memory zone for rate limiting"; + }; + + requests = lib.mkOption { + type = lib.types.int; + default = 10; + description = "Number of requests allowed per second (burst applies on top)"; + }; + + burst = lib.mkOption { + type = lib.types.int; + default = 20; + description = "Maximum burst of requests allowed beyond the rate"; + }; + }; + domains = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule { options = { @@ -52,6 +74,26 @@ in description = "Content-Security-Policy header value. Set to null to omit."; }; + rateLimit = { + enable = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + default = null; + description = "Enable rate limiting for this vhost. Defaults to global rateLimit.enable."; + }; + + requests = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + description = "Requests per second for this vhost. Defaults to global rateLimit.requests."; + }; + + burst = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + description = "Burst size for this vhost. Defaults to global rateLimit.burst."; + }; + }; + extraLocations = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule { options = { @@ -98,6 +140,14 @@ in recommendedProxySettings = true; recommendedTlsSettings = true; + # Rate limiting zones (one per domain for per-domain limits) + commonHttpConfig = lib.optionalString cfg.rateLimit.enable '' + # Global rate limiting zone + limit_req_zone $binary_remote_addr zone=global:10m rate=${toString cfg.rateLimit.requests}r/s; + # Limit connection flooding + limit_conn_zone $binary_remote_addr zone=connlimit:10m; + ''; + virtualHosts = lib.mapAttrs' (domain: opts: { name = domain; value = { @@ -127,7 +177,12 @@ in locations = { "/" = { proxyPass = "http://127.0.0.1:${toString opts.port}"; - extraConfig = opts.extraConfig; + extraConfig = opts.extraConfig + lib.optionalString (if opts.rateLimit.enable != null then opts.rateLimit.enable else cfg.rateLimit.enable) '' + # Rate limiting + limit_req zone=global burst=${toString (if opts.rateLimit.burst != null then opts.rateLimit.burst else cfg.rateLimit.burst)} nodelay; + limit_conn connlimit 30; + limit_req_status 429; + ''; }; } // lib.mapAttrs' (locPath: locOpts: { name = locPath;