feat(nginx): add rate limiting with per-domain overrides

- Global rate limit: 10 req/s with burst of 20
- Connection limit: 30 concurrent per IP
- Per-domain override support (requests, burst, enable/disable)
- SearXNG gets higher limits (20/40) to tolerate bot traffic
- Returns 429 when rate limited
This commit is contained in:
Franz Kafka 2026-03-19 15:08:34 +00:00
parent 2bc375ab86
commit 790501d290
2 changed files with 65 additions and 2 deletions

View file

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