Credit : Logo officiel
Configurer un pare-feu applicatif (WAF) avec Nginx
Configurer un pare-feu applicatif (WAF) avec Nginx
Lundi matin, j'arrive au boulot, un client me ping en urgence : son WordPress repond en 8 secondes au lieu de 200 ms et le CPU du VPS est a 100%. Je regarde les logs Nginx, et la, c'est la fete : 187 requetes par seconde sur /wp-login.php depuis une vingtaine d'IP differentes, soit du brute force distribue, soit un bot qui essaie son dictionnaire. iptables a beau filtrer le port 22, sur le 443 il laisse passer tout le monde parce qu'il ne lit pas le HTTP. C'est exactement la qu'un pare-feu applicatif (WAF) prend tout son sens : il comprend les URLs, les en-tetes, les payloads, et il peut decider qu'une IP qui tape /wp-login.php 50 fois en 10 secondes, ce n'est pas un humain.
Je te montre comment transformer un Nginx 1.24 sur Debian 12 en WAF efficace : rate limiting, geo blocking, filtres signatures, et l'integration de ModSecurity avec les regles OWASP Core Rule Set. Pas de magie, que de la conf concrete que tu peux copier-coller en production apres relecture.
Pourquoi un WAF en plus du pare-feu reseau ?
iptables, UFW, nftables, le pare-feu d'IONOS... ils travaillent en couche 3/4 (IP et TCP/UDP). Ils ne savent rien de HTTP. Un WAF, lui, lit la requete applicative complete : methode, URL, headers, body, cookies. Il peut donc detecter :
- Les injections SQL (
UNION SELECT,' OR 1=1--). - Les XSS (
<script>,javascript:). - Les traversees de repertoires (
../../etc/passwd). - Le brute force sur les pages d'authentification.
- Les scanners automatises (sqlmap, nikto, nmap).
- Les uploads de webshells PHP deguises.
Nginx integre nativement de quoi faire un WAF basique tres correct. Pour aller plus loin on lui colle ModSecurity. Les deux ensemble, c'est ceinture et bretelles. Sur les serveurs que j'admin, cette combinaison divise les logs d'erreur par cinq des la premiere semaine : tout le bruit de scan automatique disparait, et il ne reste que les vraies tentatives qui meritent qu'on s'y attarde.
Niveaux d'attaque et reponses adaptees
Une attaque classique se decompose en couches : reconnaissance (scan de ports, fingerprinting), exploration (enumeration des chemins WordPress, plugins, fichiers exposes), exploitation (injection, RCE, brute force). iptables stoppe la premiere couche en bloquant des CIDR entiers. Le rate limiting Nginx stoppe la deuxieme. ModSecurity et les patterns matching stoppent la troisieme. C'est pour ca qu'aucune brique seule ne suffit.
Cas reel : un WordPress qui prenait 30 000 hits/jour de bots
Un client e-commerce sous WordPress + WooCommerce, quatre semaines apres un changement de DNS, voit son wp-login.php se faire marteler par 30 000 requetes par jour reparties sur 800 IP differentes. Les sessions PHP-FPM saturent et les vrais clients ont des temps de reponse degrades. Sans WAF, la seule reponse possible c'est un fail2ban tres agressif qui bannit aussi des clients legitimes derriere des CGN d'operateurs mobiles. Avec rate limiting Nginx + ModSecurity en mode On + Fail2ban qui lit les blocs ModSecurity : 95% des hits sont desormais bloques au niveau Nginx sans toucher PHP, le CPU PHP-FPM redescend de 80% a 12% en moyenne, et les clients ne voient plus aucun ralentissement.
Rate limiting : la base, et ca evite 80% du bruit
La premiere brique. Limiter le nombre de requetes par client par unite de temps.
Definir les zones
Dans /etc/nginx/nginx.conf, bloc http {} :
http {
limit_req_zone $binary_remote_addr zone=global: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;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
}
Les 10m reservent 10 Mo de memoire partagee, soit environ 160 000 IP en suivi simultane. Largement assez pour un site moyen.
Appliquer dans les vhosts
server {
listen 443 ssl http2;
server_name monsite.fr;
limit_req zone=global burst=20 nodelay;
limit_conn conn_limit 10;
location = /wp-login.php {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location /wp-admin/admin-ajax.php {
limit_req zone=api burst=10 nodelay;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}
burst autorise un depassement temporaire en file d'attente. nodelay traite ces requetes immediatement au lieu de les ralentir. limit_req_status 429 renvoie un code HTTP 429 (Too Many Requests) plutot que le 503 par defaut, ce qui est plus correct semantiquement et plus agreable cote logs.
Resultat sur mon client de lundi matin : passage de 187 req/s sur wp-login a 1 req/s par IP, charge CPU divisee par 20, plus aucun ralentissement legitime.
Affiner par type de client
Tous les clients ne se valent pas. Un crawler Google legitime ne doit pas etre filtre, un cron interne non plus. La cle $binary_remote_addr ne suffit pas si tu veux des regles plus fines. Tu peux combiner avec une map qui lit le User-Agent et bypass les bots de moteurs de recherche :
map $http_user_agent $is_bot {
default 0;
"~*Googlebot" 1;
"~*Bingbot" 1;
"~*DuckDuckBot" 1;
}
map $is_bot $limit_key {
0 $binary_remote_addr;
1 "";
}
limit_req_zone $limit_key zone=global:10m rate=10r/s;
Quand $limit_key est vide, Nginx ne compte pas la requete. C'est une whitelist propre, sans CIDR a maintenir, qui survit aux changements d'IP des bots.
Geo blocking : reduire la surface d'attaque
Si ton site cible la France, le trafic depuis la Coree du Nord ou la Russie n'est probablement pas legitime. Ca ne remplace pas un WAF, mais ca elimine du bruit. Avec GeoIP2 :
apt install libnginx-mod-http-geoip2 -y
mkdir -p /usr/share/GeoIP
wget -O /tmp/dbip.mmdb.gz https://download.db-ip.com/free/dbip-country-lite-2026-03.mmdb.gz
gunzip -c /tmp/dbip.mmdb.gz > /usr/share/GeoIP/GeoLite2-Country.mmdb
Dans /etc/nginx/conf.d/geo.conf :
geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
auto_reload 24h;
$geoip2_country_code country iso_code;
}
map $geoip2_country_code $blocked_country {
default 0;
CN 1;
RU 1;
KP 1;
IR 1;
}
Dans le vhost :
server {
if ($blocked_country) {
return 444;
}
}
Le code 444 (specifique a Nginx) ferme la connexion sans reponse, ce qui est plus economique qu'un 403 quand l'IP est clairement malveillante. Attention quand meme : VPN, Tor et CDN faussent la geolocalisation. C'est un filtre, pas une frontiere.
Pour mettre a jour la base GeoIP automatiquement, j'ajoute un cron :
cat > /etc/cron.weekly/update-geoip <<'EOF'
#!/bin/bash
DATE=$(date +%Y-%m)
wget -q -O /tmp/dbip.mmdb.gz "https://download.db-ip.com/free/dbip-country-lite-${DATE}.mmdb.gz"
if [ -s /tmp/dbip.mmdb.gz ]; then
gunzip -c /tmp/dbip.mmdb.gz > /usr/share/GeoIP/GeoLite2-Country.mmdb
nginx -t && systemctl reload nginx
fi
EOF
chmod +x /etc/cron.weekly/update-geoip
La base est mise a jour mensuellement par db-ip, donc une execution hebdomadaire suffit largement.
Filtrage par patterns : bloquer le bruit signature
Les bots qui scannent les fichiers .env, les vieux backups SQL ou les .git/config, c'est constant. Bloque tout ca proprement :
server {
location ~* /\.(?:git|env|svn|hg|DS_Store|htaccess) {
deny all;
return 403;
}
location ~* \.(?:env|bak|backup|sql|log|swp|tar|gz|zip|orig|old)$ {
return 403;
}
location ~* /(?:wp-config\.php|configuration\.php|local-config\.php) {
deny all;
}
if ($http_user_agent ~* (sqlmap|nikto|nmap|masscan|zgrab|wpscan|acunetix|fimap|whatweb)) {
return 444;
}
if ($request_method !~ ^(GET|POST|HEAD|PUT|DELETE|OPTIONS)$) {
return 405;
}
client_max_body_size 25M;
client_body_buffer_size 128k;
large_client_header_buffers 4 8k;
}
Blocage des methodes exotiques (TRACE, TRACK, DEBUG) qui ne devraient jamais arriver chez toi en prod, et limitation stricte de la taille des bodies pour eviter les uploads abusifs.
ModSecurity : le WAF complet
Pour une protection avancee, ModSecurity + OWASP Core Rule Set. C'est ce que la plupart des CDN proposent en option payante, sauf que la c'est gratuit et chez toi.
Installation
apt install libmodsecurity3 libnginx-mod-http-modsecurity -y
mkdir -p /etc/nginx/modsecurity
cp /etc/modsecurity/modsecurity.conf-recommended /etc/nginx/modsecurity/modsecurity.conf
Edite /etc/nginx/modsecurity/modsecurity.conf :
SecRuleEngine DetectionOnly
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
SecAuditEngine RelevantOnly
SecAuditLog /var/log/modsec_audit.log
On commence en DetectionOnly, c'est la regle d'or pour eviter de bloquer ses propres utilisateurs des le premier deploiement.
Installer OWASP CRS
cd /etc/nginx/modsecurity
git clone https://github.com/coreruleset/coreruleset.git crs
cp crs/crs-setup.conf.example crs/crs-setup.conf
Cree un fichier d'inclusion /etc/nginx/modsecurity/main.conf :
Include /etc/nginx/modsecurity/modsecurity.conf
Include /etc/nginx/modsecurity/crs/crs-setup.conf
Include /etc/nginx/modsecurity/crs/rules/*.conf
Activer dans Nginx
server {
modsecurity on;
modsecurity_rules_file /etc/nginx/modsecurity/main.conf;
location / {
try_files $uri $uri/ /index.php?$args;
}
}
Teste la conf et reload :
nginx -t && systemctl reload nginx
Laisse tourner 3 a 7 jours en DetectionOnly, analyse /var/log/modsec_audit.log, ajoute les exceptions pour ton CMS (WordPress, PrestaShop sont connus pour declencher des regles CRS sur les payloads admin), puis passe en SecRuleEngine On.
Exceptions classiques pour WordPress
Dans /etc/nginx/modsecurity/crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf :
SecRule REQUEST_URI "@beginsWith /wp-admin/admin-ajax.php" \
"id:1000,phase:1,pass,nolog,\
ctl:ruleRemoveById=920273,\
ctl:ruleRemoveById=941100"
SecRule REQUEST_URI "@beginsWith /wp-json/" \
"id:1001,phase:1,pass,nolog,\
ctl:ruleRemoveTargetByTag=attack-rce;ARGS:content"
Niveaux de paranoia CRS
Le CRS expose un parametre tx.paranoia_level de 1 a 4. Le niveau 1 (defaut) bloque les attaques evidentes avec quasi zero faux positif. Le niveau 2 bloque des patterns plus subtils mais te demande quelques exclusions. Le niveau 3 et 4 sont reserves a des contextes ultra-sensibles (banques, sante) et generent beaucoup de faux positifs sur du contenu utilisateur normal. Pour la majorite de mes clients je reste au niveau 1, parfois je monte a 2 sur les sites e-commerce ou la stack est connue. Modifie dans crs-setup.conf :
SecAction \
"id:900000,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.paranoia_level=1"
Tester son WAF
for i in $(seq 1 50); do
curl -s -o /dev/null -w "%{http_code} " https://monsite.fr/wp-login.php
done; echo
Tu dois voir une serie de 200 puis des 429 quand tu depasses la limite.
Test injection SQL :
curl -i "https://monsite.fr/?id=1%27%20OR%201%3D1--"
Doit renvoyer 403 si ModSecurity est en On.
Test user-agent suspect :
curl -i -A "sqlmap/1.7" https://monsite.fr/
Doit renvoyer 444 (connexion fermee).
Logs en temps reel :
tail -f /var/log/nginx/error.log /var/log/modsec_audit.log
Benchmark de l'overhead
Un WAF n'est pas gratuit en CPU. Sur un VPS IONOS 2 vCPU avec un Nginx servant 200 req/s d'une page WordPress mise en cache :
Sans ModSecurity : 12% CPU, latence p95 38ms
Avec ModSecurity CRS : 18% CPU, latence p95 52ms
L'overhead est de l'ordre de 30 a 50% sur le composant Nginx, mais reste invisible cote utilisateur tant que le serveur n'est pas a la limite. Si jamais ton CPU sature sous charge, regarde du cote de SecRequestBodyAccess Off pour les routes ou tu n'as pas besoin d'inspecter le body (assets statiques notamment).
Integration avec Fail2ban et CrowdSec
Le WAF detecte et repond ponctuellement, mais ne bannit pas a long terme. Couple-le a Fail2ban qui lit les logs Nginx et bannit les IP recidivistes au niveau iptables. Crontab dediee dans /etc/fail2ban/filter.d/nginx-modsec.conf :
[Definition]
failregex = ^.*\[client <HOST>\].*ModSecurity:.*$
ignoreregex =
Et dans /etc/fail2ban/jail.local :
[nginx-modsec]
enabled = true
port = http,https
filter = nginx-modsec
logpath = /var/log/nginx/error.log
maxretry = 5
findtime = 600
bantime = 3600
CrowdSec va plus loin : il partage les IP malveillantes avec une communaute mondiale, donc tu beneficies des bans detectes ailleurs avant meme d'etre attaque. Sur mes serveurs c'est ce duo Nginx + ModSecurity + CrowdSec qui tourne, et ca a transforme le quotidien : 90% des tentatives sont bloquees au niveau reseau avant meme d'arriver a Nginx.
Comparatif rapide WAF Nginx vs alternatives
Quand on me demande pourquoi ne pas plutot prendre Cloudflare WAF, AWS WAF, ou un appliance dedie F5/Imperva, voila ma grille de lecture :
- Cloudflare WAF : excellent pour absorber les DDoS volumetriques et le filtrage edge avant que ton serveur ne voie le trafic. Mais le plan gratuit ne donne acces qu'aux Managed Rules basiques, et le vrai WAF demande l'offre Business a 200 USD/mois. Si ton budget le permet, c'est complementaire au WAF Nginx, pas concurrent.
- AWS WAF : tarification a la regle et a la requete, ca grimpe vite sur un site moyen (50 a 150 USD/mois). Pertinent si tu es deja sur ALB ou CloudFront.
- F5 / Imperva / Akamai : niveau entreprise, plusieurs milliers d'euros par mois minimum. Reserves aux gros e-commerces et secteurs regules.
- Nginx + ModSecurity + CRS : gratuit, sous ton controle, integre dans ta stack, log centralise sur ton infra. Demande quelques heures de tuning et un peu de babysitting au demarrage. C'est le rapport qualite/prix imbattable pour 99% des projets PME et freelance.
Sur les serveurs IONOS de mes clients, je tourne avec un Cloudflare gratuit en frontal pour absorber les pics et faire du caching edge, et derriere un Nginx + ModSecurity + CrowdSec sur le VPS pour la deuxieme couche. Les deux ne se marchent pas dessus tant que tu configures correctement set_real_ip_from sur les CIDR Cloudflare, sinon tu rate-limites le proxy au lieu du client final.
Erreurs courantes et leur fix
limit_req_zoneignore : tu l'as defini dans unserver {}au lieu duhttp {}. Cette directive est uniquement valide au niveau http. Deplace-la dansnginx.conf.- Tout le monde a la meme IP
127.0.0.1: ton Nginx est derriere un reverse proxy ou Cloudflare et tu n'as pas configureset_real_ip_from. Ajoutereal_ip_header CF-Connecting-IP;(Cloudflare) ouX-Forwarded-For(autre proxy) sinon ton rate limit s'applique a une seule IP : celle du proxy. - ModSecurity bloque le back-office WordPress : regles CRS 941xxx (XSS) et 942xxx (SQLi) sont sensibles aux payloads admin. Mets ton site en
DetectionOnly, identifie lesid:declenches dans les logs, et ajoute des exclusions ciblees plutot que de tout desactiver. unknown directive "modsecurity": le module n'est pas charge. Verifiels /etc/nginx/modules-enabled/et quemod-http-modsecurity.confy figure. Sinonln -s /usr/share/nginx/modules-available/mod-http-modsecurity.conf /etc/nginx/modules-enabled/.- Faux positifs sur les uploads d'images : la regle 920273 et celles de la famille 941xxx considerent parfois les binaires comme du XSS. Exclue-les sur les routes
/wp-admin/async-upload.phpou equivalent. - Geo blocking inactif : tu as oublie le
geoip2_proxysi Nginx est derriere un proxy. Ajoute la directive et la liste des CIDR de confiance. failed (24: Too many open files)sous charge : limite de file descriptors atteinte. Edite/etc/security/limits.conf(nginx soft nofile 65536) etworker_rlimit_nofile 65536;dansnginx.conf.
Pour aller plus loin
Un WAF est une couche parmi d'autres. Empile-les pour une defense en profondeur :
- Proteger son serveur du brute force avec Fail2ban — le complement parfait pour bannir les recidivistes detectes par ModSecurity.
- Configurer CrowdSec sur un serveur Linux — alternative communautaire a Fail2ban avec partage d'IP malveillantes.
- Comprendre et configurer iptables — la couche reseau sous le WAF, indispensable pour le blocage massif.
- Hardening Linux : securiser son serveur — tour d'horizon des bonnes pratiques systeme au-dela de l'applicatif.
- Mettre en place un CDN gratuit avec Cloudflare — pour absorber le DDoS volumetrique avant qu'il atteigne ton serveur.
- Reverse proxy Nginx avec SSL — pour configurer correctement le
set_real_ip_frommentionne dans les pieges.
Ton serveur, ta forteresse applicative
Un WAF Nginx bien configure, c'est trois heures de travail un samedi apres-midi et 90% du bruit malveillant qui disparait de tes logs. Le rate limiting elimine le brute force, le filtrage de patterns degage les bots paresseux, et ModSecurity en CRS gere les attaques applicatives serieuses. Combine ca avec Fail2ban ou CrowdSec qui consomme tes logs Nginx pour bannir les recidivistes au niveau iptables, et tu as une infrastructure dont la surface d'attaque est divisee par dix. Reste ensuite a maintenir tout ca a jour, surveiller les regles CRS qui evoluent, et profiter du calme.
EDIT 2026 : depuis CRS 4.0, la structure des regles a ete revue avec une separation plus nette entre regles generiques et regles dediees. Si tu mets a jour, retesta tes exclusions WordPress, certains identifiants ont change. Et si tu trouves la conf manuelle penible, regarde du cote du projet bunkerweb qui empile Nginx + ModSecurity + CrowdSec dans un Docker pre-configure : c'est plus opaque, mais ca depanne pour les setups rapides.