Limiter le débit avec Nginx et se protéger contre les DDoS

11 min de lecture·Matthieu·fail2banddos-protectionrate-limitingsecuritynginx|

Configurez le rate limiting Nginx avec limit_req, limit_conn et fail2ban pour protéger votre serveur contre les attaques par force brute et les DDoS applicatifs, sans dépendre de services tiers.

Le rate limiting est votre première ligne de défense contre les attaques par force brute, l'abus d'API et les DDoS applicatifs (couche 7). Ce tutoriel met en place trois couches de protection en utilisant uniquement Nginx et fail2ban. Pas de service anti-DDoS tiers, pas de trafic qui quitte votre serveur.

Vous allez configurer la limitation du débit de requêtes (limit_req), le throttling de connexions (limit_conn) et le bannissement automatique d'IP (fail2ban) sur Debian 12 ou Ubuntu 24.04.

Prérequis :

  • Nginx installé et fonctionnel
  • Familiarité avec la structure de configuration Nginx
  • Accès root ou sudo

Comment fonctionne le rate limiting Nginx ?

Le rate limiting Nginx utilise l'algorithme du « seau percé » (leaky bucket) via les directives limit_req_zone et limit_req. Les requêtes entrantes remplissent un seau à la vitesse à laquelle elles arrivent. Le seau se vide à un débit fixe que vous définissez. Quand le seau déborde, Nginx rejette les requêtes en excès. Cela lisse les pics de trafic tout en maintenant un débit de traitement constant.

L'implémentation repose sur deux directives. limit_req_zone définit la zone de mémoire partagée qui suit l'état de chaque client à travers tous les processus worker. limit_req applique la limite à des locations spécifiques.

# Dans le bloc http : définir la zone
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

# Dans un bloc server ou location : appliquer la limite
limit_req zone=api;

La clé $binary_remote_addr stocke chaque IP client dans un format binaire compact (4 octets pour IPv4, 16 octets pour IPv6). Une zone de 10 Mo peut contenir environ 160 000 adresses IPv4 ou 80 000 adresses IPv6. Pour la plupart des serveurs, 10m suffit largement.

Le paramètre rate accepte des requêtes par seconde (r/s) ou par minute (r/m). Nginx suit ce débit en interne en millisecondes. Un débit de 10r/s signifie qu'une requête est autorisée toutes les 100 ms.

Comment configurer limit_req_zone et limit_req ?

Créez un fichier de configuration dédié au rate limiting pour garder les choses modulaires :

sudo nano /etc/nginx/conf.d/rate-limiting.conf
# Zones de mémoire partagée - définies au niveau 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;

# Retourner 429 au lieu du 503 par défaut
limit_req_status 429;

# Journaliser les événements de rate limiting au niveau warn (les retards au niveau notice)
limit_req_log_level warn;

Appliquez ensuite les zones dans votre bloc server :

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

    # Limite de débit générale pour toutes les requêtes
    limit_req zone=general burst=20 nodelay;

    location /login {
        # Limite stricte sur le endpoint de login
        limit_req zone=login burst=3 nodelay;
        proxy_pass http://127.0.0.1:3000;
    }

    location /api/ {
        # Limite plus élevée pour les consommateurs d'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;
    }
}

Testez la configuration et rechargez :

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

Quand une directive limit_req est définie dans un bloc location, elle remplace tout limit_req hérité du niveau server. La zone general s'applique à / mais pas à /login ni à /api/ puisque ces locations ont leurs propres directives limit_req. Si vous avez besoin que les deux zones s'appliquent, ajoutez plusieurs lignes limit_req dans le même bloc.

Que font burst, nodelay et delay ?

Le paramètre burst contrôle combien de requêtes en excès Nginx met en file d'attente au lieu de les rejeter directement. Sans burst, toute requête dépassant le débit reçoit un 429. Avec burst, Nginx retient les requêtes excédentaires dans une file et les libère au débit de base.

Paramètre Requêtes immédiates Requêtes en file Rejetées Cas d'usage
burst=0 (défaut) 1 par intervalle Aucune Tout au-delà du débit Limites d'API strictes
burst=5 1 par intervalle Jusqu'à 5, libérées au débit de base Au-delà de burst+1 Soumissions de formulaires
burst=5 nodelay Jusqu'à 6 d'un coup Aucune en file, mais les slots de burst se rechargent au débit de base Au-delà de burst+1 jusqu'au rechargement Pages de login, trafic général
burst=20 delay=10 Jusqu'à 11 d'un coup Requêtes 12-21 ralenties au débit de base Au-delà de burst+1 APIs avec pics occasionnels

Avec burst=5 (sans nodelay), si 6 requêtes arrivent simultanément, la requête 1 est traitée immédiatement. Les requêtes 2-6 sont mises en file et libérées une par intervalle (toutes les 100 ms à 10r/s). La dernière requête en file attend 500 ms. Cela ajoute de la latence mais ne rejette jamais les pics légitimes.

Avec burst=5 nodelay, les 6 requêtes sont traitées immédiatement. Mais les 5 slots de burst mettent 500 ms à se recharger. Si 6 nouvelles requêtes arrivent 200 ms plus tard, seuls 3 slots se sont rechargés, donc 3 requêtes excédentaires sont rejetées.

Avec burst=20 delay=10, les 11 premières requêtes (1 de base + 10 seuil de delay) sont traitées sans attente. Les requêtes 12-21 sont ralenties au débit de base. Tout ce qui dépasse 21 est rejeté. Ce mode hybride fonctionne bien pour les APIs qui reçoivent des pics périodiques de clients batch légitimes.

Comment limiter le débit de différents endpoints séparément ?

Définissez des zones distinctes avec des clés différentes pour appliquer des limites indépendantes. L'exemple ci-dessus utilise déjà trois zones. Vous pouvez aussi limiter le débit par chemin d'URI en utilisant $uri comme clé :

# Rate limiting par URI : limite le total des requêtes vers chaque URI unique
limit_req_zone $uri zone=per_uri:10m rate=50r/s;

C'est utile quand certains endpoints (comme une page de recherche ou une fonction d'export) nécessitent un throttling global, quel que soit le client qui les appelle.

Pour un rate limiting basé sur une clé API, utilisez map pour extraire la clé d'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;

Si le client envoie un header X-API-Key, la limite se base sur cette clé. Sinon, elle revient à une limitation par IP.

Comment limiter les connexions avec limit_conn ?

Alors que limit_req contrôle le débit de requêtes, limit_conn plafonne le nombre de connexions simultanées par client. Cette directive est efficace contre les attaques slowloris et l'abus de téléchargement.

# Dans le bloc http
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn_status 429;
limit_conn_log_level warn;
# Dans un bloc server ou location
server {
    # Max 20 connexions simultanées par IP
    limit_conn addr 20;

    location /downloads/ {
        # Max 2 téléchargements simultanés par IP
        limit_conn addr 2;
        limit_rate 1m;  # Limiter aussi la bande passante à 1 Mo/s par connexion
    }
}

Note pour HTTP/2 et HTTP/3 : chaque requête concurrente compte comme une connexion distincte. Un navigateur chargeant une page avec 30 ressources via une seule connexion HTTP/2 compte comme 30 connexions pour limit_conn. Fixez la limite plus haut que pour HTTP/1.1.

limit_conn et limit_req sont complémentaires. Utilisez les deux. limit_req bloque les rafales de requêtes. limit_conn bloque les inondations de connexions.

Comment retourner une page d'erreur 429 personnalisée ?

Par défaut, les requêtes limitées reçoivent une page d'erreur générique. Une page 429 personnalisée peut inclure un header Retry-After et un message lisible.

Créez la page d'erreur :

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

Ajoutez la page d'erreur et le header Retry-After à votre bloc server :

server {
    #... directives de rate limiting...

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

La directive internal empêche l'accès direct à la page d'erreur. Le mot-clé always sur add_header garantit que le header est envoyé même sur les réponses d'erreur. La valeur de Retry-After (en secondes) indique aux clients bien comportés quand réessayer.

Comment tester les limites en toute sécurité avec dry_run ?

Activez limit_req_dry_run on pour simuler le rate limiting sans rejeter de requêtes. Nginx journalise ce qu'il aurait fait, mais toutes les requêtes passent. Cette option est disponible depuis Nginx 1.17.1.

server {
    limit_req zone=general burst=20 nodelay;
    limit_req_dry_run on;  # Journaliser seulement, ne pas appliquer

    # Ajouter le statut de rate limiting aux logs d'accès
    #...
}

Ajoutez $limit_req_status à votre format de log pour suivre les événements dry run dans les logs d'accès :

# Dans le bloc 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';

# Dans le bloc server
access_log /var/log/nginx/access.log ratelimit;

Le processus dry_run :

  1. Ajoutez limit_req_dry_run on; à votre configuration
  2. Rechargez Nginx
  3. Générez du trafic de test (voir la section de test ci-dessous)
  4. Vérifiez le log d'erreur pour les entrées 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"

Le niveau de log ici est [warn] à cause de la directive limit_req_log_level warn. Assurez-vous que votre directive error_log inclut le niveau warn ou inférieur, sinon ces messages n'apparaîtront pas. La configuration de production avec error_log /var/log/nginx/example.error.log warn; gère cela.

  1. Vérifiez les logs d'accès pour la variable $limit_req_status :
sudo grep "REJECTED_DRY_RUN\|DELAYED_DRY_RUN" /var/log/nginx/access.log

La variable $limit_req_status retourne l'une des valeurs suivantes : PASSED, DELAYED, REJECTED, DELAYED_DRY_RUN ou REJECTED_DRY_RUN.

  1. Quand les débits semblent corrects, supprimez la ligne limit_req_dry_run et rechargez.

Comment mettre en liste blanche les IP de confiance ?

Utilisez un bloc geo pour exclure les systèmes de monitoring, les load balancers ou les IP de votre bureau du rate limiting :

# Dans le bloc http
geo $limit {
    default 1;
    10.0.0.0/8      0;  # Réseau interne
    192.168.0.0/16   0;  # Réseau interne
    203.0.113.10     0;  # Serveur de monitoring
}

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

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

Quand $limit_key est une chaîne vide, Nginx ignore le rate limiting pour cette requête. Les IP correspondant au bloc geo obtiennent $limit = 0, qui se traduit par une clé vide.

Si vous voulez que les IP de confiance aient un débit plus élevé plutôt qu'aucune 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;
}

Toutes les IP correspondent à trusted, mais seules les IP non listées correspondent à general. La limite la plus restrictive s'applique, donc les IP de confiance sont limitées à 100 r/s tandis que les autres obtiennent 10 r/s.

Comment bannir les récidivistes avec fail2ban ?

Le rate limiting rejette des requêtes individuelles, mais les attaquants persistants reviennent sans cesse. fail2ban surveille le log d'erreur Nginx et bannit les IP au niveau du pare-feu après des violations répétées.

Installez fail2ban si ce n'est pas déjà fait :

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 inclut un filtre nginx-limit-req prêt à l'emploi. La regex du filtre correspond aux lignes comme :

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

Créez la configuration du jail. Ne modifiez jamais les fichiers .conf directement ; utilisez les fichiers .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

Cela bannit une IP pendant 10 minutes après 10 violations de rate limiting en 60 secondes.

Pour des bannissements progressifs, ajoutez un second jail dans le même fichier :

[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

Le premier jail attrape les rafales courtes (10 hits en une minute = bannissement de 10 minutes). Le second attrape les récidivistes (30 hits en une heure = bannissement de 24 heures).

Redémarrez fail2ban et vérifiez le statut du jail :

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

Sur Debian 12 et les systèmes plus anciens, la sortie ressemble à ceci :

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:

Sur Ubuntu 24.04, fail2ban utilise par défaut le backend systemd journal (backend = auto résout en systemd). La sortie affiche Journal matches: au lieu de 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:

Les deux backends fonctionnent. Le backend journal lit les mêmes messages de log Nginx via systemd. Si vous préférez la surveillance par fichier, ajoutez backend = pyinotify à la section du jail.

Pour débannir manuellement une IP pendant les tests :

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

Le log du jail se trouve dans :

sudo journalctl -u fail2ban -f

Comment vérifier que le rate limiting fonctionne ?

Testez depuis une machine extérieure au serveur. Ne testez jamais depuis localhost car 127.0.0.1 peut être dans votre liste blanche.

Test rapide avec une boucle curl :

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

Avec un débit de 10r/s et burst=20 nodelay, les 21 premières requêtes retournent 200. Une fois le burst épuisé, les réponses passent à 429.

Test de charge avec 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

Le compteur Non-2xx or 3xx responses montre combien de requêtes Nginx a limitées. Ici, 18 241 requêtes sur 20 384 ont reçu un 429.

Vérifiez le log d'erreur pendant le 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"

La valeur excess montre de combien la requête a dépassé la limite. Des valeurs élevées indiquent un trafic plus agressif.

Configuration de production complète

La configuration de rate limiting complète combinant les trois couches :

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

# --- Liste blanche ---
geo $limit {
    default 1;
    10.0.0.0/8      0;
    192.168.0.0/16   0;
    # Ajoutez vos IP de monitoring / bureau ici
}

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

# --- Zones de débit de requêtes ---
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;

# --- Zone de connexions ---
limit_conn_zone $binary_remote_addr zone=addr:10m;

# --- Codes de réponse et journalisation ---
limit_req_status 429;
limit_conn_status 429;
limit_req_log_level warn;
limit_conn_log_level warn;

# --- Log d'accès avec statut de 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;

    # Limites globales
    limit_req zone=general burst=20 nodelay;
    limit_conn addr 30;

    # Page 429 personnalisée
    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

Après avoir écrit tous les fichiers de configuration :

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

Pour une configuration de sécurité plus large incluant les headers, TLS et d'autres mesures de durcissement, consultez.

Référence des directives

Directive Contexte Défaut Depuis
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

Dépannage

Le rate limiting ne fonctionne pas du tout : Vérifiez que limit_req_zone est dans le bloc http, pas à l'intérieur d'un bloc server. Les zones doivent être définies avant d'être référencées. Si votre configuration utilise des directives include, assurez-vous que le fichier de zones est inclus avant les blocs server.

Les utilisateurs légitimes reçoivent des 429 : Réduisez le rate, augmentez le burst, ou passez de pas-de-burst à burst=N nodelay. Utilisez le mode dry_run pour mesurer le trafic réel avant de fixer les limites. Vérifiez si le multiplexage HTTP/2 gonfle les compteurs limit_conn.

fail2ban ne bannit pas : Confirmez que le logpath correspond à l'emplacement réel des logs d'erreur Nginx. Vérifiez que limit_req_log_level est défini à warn ou error (la valeur par défaut). Vérifiez que le jail est actif avec sudo fail2ban-client status nginx-limit-req. Si vous testez depuis localhost, notez que fail2ban ignore l'IP du serveur par défaut (ignoreself = true). Testez depuis une machine externe.

Les messages de rate limiting n'apparaissent pas dans le log d'erreur : La directive error_log est par défaut au niveau error, ce qui filtre les messages de niveau warn de limit_req_log_level warn. Définissez error_log /var/log/nginx/error.log warn; dans votre bloc server pour voir les événements de rate limiting.

Mémoire de zone épuisée : Une zone de 10m contient environ 160 000 états IPv4. Si vous voyez could not allocate node dans les logs, augmentez la taille de la zone. Surveillez l'utilisation de la zone au fil du temps avec $limit_req_status dans vos logs d'accès.

Derrière un load balancer ou CDN : Si Nginx ne voit que l'IP du load balancer, le rate limiting s'applique à cette seule IP. Utilisez $http_x_forwarded_for ou $realip_remote_addr comme clé de zone au lieu de $binary_remote_addr. Vous devez aussi configurer set_real_ip_from avec la plage d'IP du load balancer. Ne faites confiance à X-Forwarded-For que depuis des proxies connus, car les clients peuvent le falsifier.

Les logs sont votre outil principal de débogage :

# Suivre les événements de rate limiting en temps réel
sudo tail -f /var/log/nginx/error.log | grep "limiting"

# Vérifier les actions fail2ban
sudo journalctl -u fail2ban -f

Copyright 2026 Virtua.Cloud. Tous droits réservés. Ce contenu est une création originale de l'équipe Virtua.Cloud. Toute reproduction, republication ou redistribution sans autorisation écrite est interdite.

Prêt à essayer ?

Déployez votre serveur en quelques secondes. Linux, Windows ou FreeBSD.

Voir les offres VPS