Rate Limiting con Nginx e protezione DDoS

11 min di lettura·Matthieu·fail2banddos-protectionrate-limitingsecuritynginx|

Configura il rate limiting di Nginx con limit_req, limit_conn e fail2ban per proteggere il tuo server da attacchi brute-force e DDoS a livello applicativo, senza dipendere da servizi di terze parti.

Il rate limiting è la prima linea di difesa contro attacchi brute-force, abuso delle API e DDoS a livello applicativo. Questo tutorial costruisce tre livelli di protezione usando solo Nginx e fail2ban. Nessun servizio anti-DDoS di terze parti, nessun traffico che lascia il tuo server.

Configurerai la limitazione della frequenza delle richieste (limit_req), il throttling delle connessioni (limit_conn) e il ban automatico degli IP (fail2ban) su Debian 12 o Ubuntu 24.04.

Prerequisiti:

  • Nginx installato e in esecuzione
  • Familiarità con la struttura di configurazione di Nginx
  • Accesso root o sudo

Come funziona il rate limiting di Nginx?

Il rate limiting di Nginx usa l'algoritmo leaky bucket tramite le direttive limit_req_zone e limit_req. Le richieste in arrivo riempiono un secchio a qualsiasi velocità. Il secchio si svuota a una frequenza fissa definita da te. Quando il secchio trabocca, Nginx rifiuta le richieste in eccesso. Questo smussa i picchi di traffico mantenendo una frequenza di elaborazione costante.

L'implementazione coinvolge due direttive. limit_req_zone definisce la zona di memoria condivisa che traccia lo stato di ogni client attraverso tutti i processi worker. limit_req applica il limite a location specifiche.

# Nel blocco http: definire la zona
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

# In un blocco server o location: applicare il limite
limit_req zone=api;

La chiave $binary_remote_addr memorizza ogni IP client in formato binario compatto (4 byte per IPv4, 16 byte per IPv6). Una zona da 10 MB contiene circa 160.000 indirizzi IPv4 o 80.000 indirizzi IPv6. Per la maggior parte dei server, 10m è sufficiente.

Il parametro rate accetta richieste al secondo (r/s) o al minuto (r/m). Nginx lo traccia internamente in millisecondi. Un rate di 10r/s significa una richiesta consentita ogni 100 ms.

Come configuro limit_req_zone e limit_req?

Crea un file di configurazione dedicato al rate limiting per mantenere la modularità:

sudo nano /etc/nginx/conf.d/rate-limiting.conf
# Zone di memoria condivisa - definite a livello http
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;

# Restituire 429 invece del 503 predefinito
limit_req_status 429;

# Registrare gli eventi di rate limiting a livello warn (i ritardi a livello notice)
limit_req_log_level warn;

Poi applica le zone nel tuo blocco server:

sudo nano /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com;

    # Limite di frequenza generale per tutte le richieste
    limit_req zone=general burst=20 nodelay;

    location /login {
        # Limite stretto sull'endpoint di login
        limit_req zone=login burst=3 nodelay;
        proxy_pass http://127.0.0.1:3000;
    }

    location /api/ {
        # Limite più alto per i consumatori API
        limit_req zone=api burst=50 delay=30;
        proxy_pass http://127.0.0.1:3000;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

Testa la configurazione e ricarica:

sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx

Quando una direttiva limit_req è definita in un blocco location, sovrascrive qualsiasi limit_req ereditato dal livello server. La zona general si applica a / ma non a /login o /api/ perché quelle location hanno le proprie direttive limit_req. Se hai bisogno che entrambe le zone si applichino, aggiungi più righe limit_req nello stesso blocco.

Cosa fanno burst, nodelay e delay?

Il parametro burst controlla quante richieste in eccesso Nginx mette in coda invece di rifiutarle subito. Senza burst, qualsiasi richiesta oltre il rate riceve un 429. Con burst, Nginx trattiene le richieste in eccesso in una coda e le rilascia al rate base.

Parametro Richieste immediate Richieste in coda Rifiutate Caso d'uso
burst=0 (predefinito) 1 per intervallo Nessuna Tutto oltre il rate Limiti API stretti
burst=5 1 per intervallo Fino a 5, rilasciate al rate base Oltre burst+1 Invio di form
burst=5 nodelay Fino a 6 contemporaneamente Nessuna in coda, ma gli slot burst si ricaricano al rate base Oltre burst+1 fino alla ricarica Pagine di login, traffico generale
burst=20 delay=10 Fino a 11 contemporaneamente Richieste 12-21 rallentate al rate base Oltre burst+1 API con picchi occasionali

Con burst=5 (senza nodelay), se 6 richieste arrivano simultaneamente, la richiesta 1 viene elaborata subito. Le richieste 2-6 si accodano e vengono rilasciate una per intervallo (ogni 100 ms a 10r/s). L'ultima richiesta in coda attende 500 ms. Questo aggiunge latenza ma non scarta mai i burst legittimi.

Con burst=5 nodelay, tutte e 6 le richieste vengono elaborate immediatamente. Ma i 5 slot burst impiegano 500 ms per ricaricarsi. Se 200 ms dopo arrivano altre 6 richieste, solo 3 slot si sono ricaricati, quindi 3 richieste in eccesso vengono rifiutate.

Con burst=20 delay=10, le prime 11 richieste (1 base + 10 soglia delay) vengono elaborate senza attesa. Le richieste 12-21 vengono rallentate al rate base. Tutto ciò che supera 21 viene rifiutato. Questa modalità ibrida funziona bene per le API che ricevono picchi periodici da client batch legittimi.

Come limito endpoint diversi separatamente?

Definisci zone separate con chiavi diverse per applicare limiti indipendenti. L'esempio sopra usa già tre zone. Puoi anche limitare per percorso URI usando $uri come chiave:

# Rate limiting per URI: limita le richieste totali a ogni URI unico
limit_req_zone $uri zone=per_uri:10m rate=50r/s;

È utile quando certi endpoint (come una pagina di ricerca o una funzione di export) necessitano di throttling globale indipendentemente dal client che li chiama.

Per rate limiting basato su chiave API, usa map per estrarre la chiave da un header:

map $http_x_api_key $api_key_limit {
    default          $binary_remote_addr;
    "~^.+$"          $http_x_api_key;
}

limit_req_zone $api_key_limit zone=api_keyed:10m rate=100r/s;

Se il client invia un header X-API-Key, il rate limit si basa su quella chiave. Altrimenti ricade sulla limitazione per IP.

Come limito le connessioni con limit_conn?

Mentre limit_req controlla la frequenza delle richieste, limit_conn limita il numero di connessioni simultanee per client. Funziona bene contro attacchi slowloris e abuso di download.

# Nel blocco http
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn_status 429;
limit_conn_log_level warn;
# In un blocco server o location
server {
    # Max 20 connessioni simultanee per IP
    limit_conn addr 20;

    location /downloads/ {
        # Max 2 download simultanei per IP
        limit_conn addr 2;
        limit_rate 1m;  # Limitare anche la banda a 1 MB/s per connessione
    }
}

Nota per HTTP/2 e HTTP/3: ogni richiesta concorrente conta come una connessione separata. Un browser che carica una pagina con 30 risorse tramite una singola connessione HTTP/2 conta come 30 connessioni per limit_conn. Imposta il limite più alto rispetto a HTTP/1.1.

limit_conn e limit_req sono complementari. Usali entrambi. limit_req blocca le raffiche di richieste. limit_conn blocca le inondazioni di connessioni.

Come restituisco una pagina di errore 429 personalizzata?

Di default, le richieste limitate ricevono una pagina di errore generica. Una pagina 429 personalizzata può includere un header Retry-After e un messaggio leggibile.

Crea la pagina di errore:

sudo mkdir -p /var/www/error
sudo nano /var/www/error/429.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>429 - Too Many Requests</title>
    <style>
        body { font-family: system-ui, sans-serif; text-align: center; padding: 5rem 1rem; }
        h1 { font-size: 2rem; }
        p { color: #555; }
    </style>
</head>
<body>
    <h1>429 - Too Many Requests</h1>
    <p>You have exceeded the request limit. Wait a moment and try again.</p>
</body>
</html>
sudo chmod 644 /var/www/error/429.html

Aggiungi la pagina di errore e l'header Retry-After al tuo blocco server:

server {
    #... direttive di rate limiting...

    error_page 429 /429.html;
    location = /429.html {
        root /var/www/error;
        internal;
        add_header Retry-After 5 always;
    }
}

La direttiva internal impedisce l'accesso diretto alla pagina di errore. La parola chiave always su add_header assicura che l'header venga inviato anche nelle risposte di errore. Il valore Retry-After (in secondi) indica ai client ben comportati quando riprovare.

Come testo i rate limit in sicurezza con dry_run?

Abilita limit_req_dry_run on per simulare il rate limiting senza rifiutare richieste. Nginx registra cosa avrebbe fatto, ma tutte le richieste passano. Questa opzione è disponibile da Nginx 1.17.1.

server {
    limit_req zone=general burst=20 nodelay;
    limit_req_dry_run on;  # Solo registrare, non applicare

    # Aggiungere lo stato di rate limiting ai log di accesso
    #...
}

Aggiungi $limit_req_status al tuo formato di log per tracciare gli eventi dry run nei log di accesso:

# Nel blocco http
log_format ratelimit '$remote_addr - $remote_user [$time_local] '
                     '"$request" $status $body_bytes_sent '
                     '"$http_referer" "$http_user_agent" '
                     'rate_limit=$limit_req_status';

# Nel blocco server
access_log /var/log/nginx/access.log ratelimit;

Il flusso dry_run:

  1. Aggiungi limit_req_dry_run on; alla configurazione
  2. Ricarica Nginx
  3. Genera traffico di test (vedi la sezione test sotto)
  4. Controlla il log di errore per le voci dry run:
sudo grep "dry run" /var/log/nginx/error.log
2026/03/19 14:22:31 [warn] 1234#1234: *567 limiting requests, dry run, excess: 1.532 by zone "general", client: 203.0.113.50, server: example.com, request: "GET / HTTP/1.1", host: "example.com"

Il livello di log qui è [warn] a causa della direttiva limit_req_log_level warn. Assicurati che la tua direttiva error_log includa il livello warn o inferiore, altrimenti questi messaggi non appariranno. La configurazione di produzione con error_log /var/log/nginx/example.error.log warn; gestisce questo aspetto.

  1. Controlla i log di accesso per la variabile $limit_req_status:
sudo grep "REJECTED_DRY_RUN\|DELAYED_DRY_RUN" /var/log/nginx/access.log

La variabile $limit_req_status restituisce uno dei seguenti valori: PASSED, DELAYED, REJECTED, DELAYED_DRY_RUN o REJECTED_DRY_RUN.

  1. Quando i rate sembrano corretti, rimuovi la riga limit_req_dry_run e ricarica.

Come inserisco gli IP fidati in una whitelist?

Usa un blocco geo per escludere sistemi di monitoraggio, load balancer o IP dell'ufficio dal rate limiting:

# Nel blocco http
geo $limit {
    default 1;
    10.0.0.0/8      0;  # Rete interna
    192.168.0.0/16   0;  # Rete interna
    203.0.113.10     0;  # Server di monitoraggio
}

map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}

limit_req_zone $limit_key zone=general:10m rate=10r/s;

Quando $limit_key è una stringa vuota, Nginx salta completamente il rate limiting per quella richiesta. Gli IP che corrispondono al blocco geo ottengono $limit = 0, che viene mappato a una chiave vuota.

Se vuoi che gli IP in whitelist abbiano un rate più alto invece di nessun limite:

limit_req_zone $limit_key zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=trusted:1m rate=100r/s;

server {
    limit_req zone=general burst=20 nodelay;
    limit_req zone=trusted burst=100 nodelay;
}

Tutti gli IP corrispondono a trusted, ma solo gli IP non in whitelist corrispondono a general. Si applica il limite più restrittivo, quindi gli IP in whitelist sono limitati a 100 r/s mentre gli altri ottengono 10 r/s.

Come blocco i recidivi con fail2ban?

Il rate limiting rifiuta richieste singole, ma gli attaccanti persistenti continuano a tornare. fail2ban monitora il log di errore di Nginx e blocca gli IP a livello firewall dopo violazioni ripetute.

Installa fail2ban se non l'hai già fatto:

sudo apt update && sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban
 fail2ban.service - Fail2Ban Service
     Loaded: loaded (/usr/lib/systemd/system/fail2ban.service; enabled; preset: enabled)
     Active: active (running) since Wed 2026-03-19 14:30:00 UTC; 2s ago

fail2ban include un filtro integrato nginx-limit-req. La regex del filtro corrisponde a righe come:

limiting requests, excess: 1.532 by zone "general", client: 203.0.113.50

Crea la configurazione del jail. Non modificare mai i file .conf direttamente; usa gli override .local:

sudo nano /etc/fail2ban/jail.local
[nginx-limit-req]
enabled  = true
port     = http,https
filter   = nginx-limit-req
logpath  = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime  = 600

Questo blocca un IP per 10 minuti dopo 10 violazioni di rate limiting in 60 secondi.

Per ban progressivi, aggiungi un secondo jail nello stesso file:

[nginx-limit-req-repeat]
enabled  = true
port     = http,https
filter   = nginx-limit-req
logpath  = /var/log/nginx/error.log
maxretry = 30
findtime = 3600
bantime  = 86400

Il primo jail cattura i burst brevi (10 hit in un minuto = ban di 10 minuti). Il secondo cattura i recidivi (30 hit in un'ora = ban di 24 ore).

Riavvia fail2ban e controlla lo stato del jail:

sudo systemctl restart fail2ban
sudo fail2ban-client status nginx-limit-req

Su Debian 12 e sistemi precedenti, l'output appare così:

Status for the jail: nginx-limit-req
|- Filter
|  |- Currently failed:	0
|  |- Total failed:	0
|  `- File list:	/var/log/nginx/error.log
`- Actions
   |- Currently banned:	0
   |- Total banned:	0
   `- Banned IP list:

Su Ubuntu 24.04, fail2ban usa di default il backend journal di systemd (backend = auto si risolve in systemd). L'output mostra Journal matches: invece di File list::

Status for the jail: nginx-limit-req
|- Filter
|  |- Currently failed:	0
|  |- Total failed:	0
|  `- Journal matches:	_SYSTEMD_UNIT=nginx.service + _COMM=nginx
`- Actions
   |- Currently banned:	0
   |- Total banned:	0
   `- Banned IP list:

Entrambi i backend funzionano. Il backend journal legge gli stessi messaggi di log Nginx tramite systemd. Se preferisci il monitoraggio basato su file, aggiungi backend = pyinotify alla sezione del jail.

Per sbloccare manualmente un IP durante i test:

sudo fail2ban-client set nginx-limit-req unbanip 203.0.113.50

Il log del jail si trova in:

sudo journalctl -u fail2ban -f

Come verifico che il rate limiting funziona?

Testa da una macchina esterna al server. Non testare mai da localhost perché 127.0.0.1 potrebbe essere nella tua whitelist.

Test rapido con un loop curl:

for i in $(seq 1 20); do
    curl -s -o /dev/null -w "%{http_code}\n" https://example.com/
done

Con un rate di 10r/s e burst=20 nodelay, le prime 21 richieste restituiscono 200. Una volta esaurito il burst, le risposte passano a 429.

Test di carico con wrk:

sudo apt install -y wrk
wrk -t2 -c10 -d10s https://example.com/
Running 10s test @ https://example.com/
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     5.23ms    2.11ms  28.44ms   75.32%
    Req/Sec     1.02k   121.33     1.34k    68.00%
  20384 requests in 10.01s, 15.22MB read
  Non-2xx or 3xx responses: 18241
Requests/sec:   2036.36
Transfer/sec:      1.52MB

Il contatore Non-2xx or 3xx responses mostra quante richieste Nginx ha limitato. Qui, 18.241 su 20.384 richieste hanno ricevuto un 429.

Controlla il log di errore durante il test:

sudo tail -f /var/log/nginx/error.log
2026/03/19 14:45:12 [warn] 1234#1234: *890 limiting requests, excess: 9.876 by zone "general", client: 203.0.113.50, server: example.com, request: "GET / HTTP/1.1", host: "example.com"

Il valore excess mostra di quanto la richiesta ha superato il limite. Valori più alti indicano traffico più aggressivo.

Configurazione di produzione completa

La configurazione completa di rate limiting che combina tutti e tre i livelli:

# /etc/nginx/conf.d/rate-limiting.conf

# --- Whitelist ---
geo $limit {
    default 1;
    10.0.0.0/8      0;
    192.168.0.0/16   0;
    # Aggiungi qui i tuoi IP di monitoraggio / ufficio
}

map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}

# --- Zone di frequenza richieste ---
limit_req_zone $limit_key zone=general:10m rate=10r/s;
limit_req_zone $limit_key zone=login:10m rate=1r/s;
limit_req_zone $limit_key zone=api:10m rate=30r/s;

# --- Zona connessioni ---
limit_conn_zone $binary_remote_addr zone=addr:10m;

# --- Codici di risposta e logging ---
limit_req_status 429;
limit_conn_status 429;
limit_req_log_level warn;
limit_conn_log_level warn;

# --- Log di accesso con stato rate limiting ---
log_format ratelimit '$remote_addr - $remote_user [$time_local] '
                     '"$request" $status $body_bytes_sent '
                     '"$http_referer" "$http_user_agent" '
                     'rate_limit=$limit_req_status';
# /etc/nginx/sites-available/example.com

server {
    listen 80;
    server_name example.com;

    access_log /var/log/nginx/example.access.log ratelimit;
    error_log /var/log/nginx/example.error.log warn;

    # Limiti globali
    limit_req zone=general burst=20 nodelay;
    limit_conn addr 30;

    # Pagina 429 personalizzata
    error_page 429 /429.html;
    location = /429.html {
        root /var/www/error;
        internal;
        add_header Retry-After 5 always;
    }

    location /login {
        limit_req zone=login burst=3 nodelay;
        limit_conn addr 5;
        proxy_pass http://127.0.0.1:3000;
    }

    location /api/ {
        limit_req zone=api burst=50 delay=30;
        limit_conn addr 20;
        proxy_pass http://127.0.0.1:3000;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}
# /etc/fail2ban/jail.local

[nginx-limit-req]
enabled  = true
port     = http,https
filter   = nginx-limit-req
logpath  = /var/log/nginx/example.error.log
maxretry = 10
findtime = 60
bantime  = 600

[nginx-limit-req-repeat]
enabled  = true
port     = http,https
filter   = nginx-limit-req
logpath  = /var/log/nginx/example.error.log
maxretry = 30
findtime = 3600
bantime  = 86400

Dopo aver scritto tutti i file di configurazione:

sudo nginx -t && sudo systemctl reload nginx
sudo systemctl restart fail2ban

Per una configurazione di sicurezza più ampia che includa header, TLS e altre misure di hardening, vedi.

Riferimento direttive

Direttiva Contesto Predefinito Da
limit_req_zone http - 0.7.21
limit_req http, server, location - 0.7.21
limit_req_status http, server, location 503 1.3.15
limit_req_log_level http, server, location error 0.8.18
limit_req_dry_run http, server, location off 1.17.1
limit_conn_zone http - 1.1.8
limit_conn http, server, location - 0.7.21
limit_conn_status http, server, location 503 1.3.15
limit_conn_log_level http, server, location error 0.8.18
limit_conn_dry_run http, server, location off 1.17.6

Risoluzione dei problemi

Il rate limiting non funziona affatto: Verifica che limit_req_zone sia nel blocco http, non dentro un blocco server. Le zone devono essere definite prima di essere referenziate. Se la configurazione usa direttive include, assicurati che il file delle zone sia incluso prima dei blocchi server.

Utenti legittimi ricevono 429: Abbassa il rate, aumenta il burst, o passa da nessun burst a burst=N nodelay. Usa la modalità dry_run per misurare i pattern di traffico reali prima di fissare i limiti. Verifica se il multiplexing HTTP/2 sta gonfiando i contatori limit_conn.

fail2ban non blocca: Conferma che il logpath corrisponda a dove Nginx scrive effettivamente i log di errore. Verifica che limit_req_log_level sia impostato su warn o error (il valore predefinito). Controlla che il jail sia attivo con sudo fail2ban-client status nginx-limit-req. Se stai testando da localhost, nota che fail2ban ignora l'IP del server stesso di default (ignoreself = true). Testa da una macchina esterna.

I messaggi di rate limit non compaiono nel log di errore: La direttiva error_log ha come default il livello error, che filtra i messaggi di livello warn da limit_req_log_level warn. Imposta error_log /var/log/nginx/error.log warn; nel tuo blocco server per vedere gli eventi di rate limiting.

Memoria della zona esaurita: Una zona da 10m contiene circa 160.000 stati IPv4. Se vedi could not allocate node nei log, aumenta la dimensione della zona. Monitora l'uso della zona nel tempo con $limit_req_status nei tuoi log di accesso.

Dietro un load balancer o CDN: Se Nginx vede solo l'IP del load balancer, il rate limiting si applica a quell'unico IP. Usa $http_x_forwarded_for o $realip_remote_addr come chiave di zona invece di $binary_remote_addr. Devi anche configurare set_real_ip_from con il range IP del load balancer. Fidati di X-Forwarded-For solo da proxy conosciuti perché i client possono falsificarlo.

I log sono il tuo strumento principale di debug:

# Seguire gli eventi di rate limiting in tempo reale
sudo tail -f /var/log/nginx/error.log | grep "limiting"

# Controllare le azioni di fail2ban
sudo journalctl -u fail2ban -f

Copyright 2026 Virtua.Cloud. Tutti i diritti riservati. Questo contenuto è un'opera originale del team Virtua.Cloud. La riproduzione, ripubblicazione o redistribuzione senza autorizzazione scritta è vietata.

Pronto a provare?

Distribuisci il tuo server in pochi secondi. Linux, Windows o FreeBSD.

Vedi piani VPS