En août 2026, un site corporate WordPress dont nous assurons l’exploitation est tombé sous une attaque DDoS applicative. Pic mesuré : 10 713 requêtes sur une seule minute, toutes dirigées vers la page d’accueil. Load average à 24, swap saturé à 100 %, 3 875 connexions TCP établies simultanément. Il nous a fallu 48 heures pour remettre le service en ligne, avec un WAF BunkerWeb en reverse proxy devant OpenLiteSpeed et un challenge JavaScript sur l’ensemble du trafic.
Cet article est le retour d’incident complet. Pas un tutoriel BunkerWeb, il en existe déjà de très bons, mais la liste des choses qui ont marché, de celles qui ont échoué, et des cinq pièges qui ont failli faire tomber le site une seconde fois de notre propre fait. Le contexte client est anonymisé : site vitrine multilingue, thème Avada, une quarantaine d’extensions, OpenLiteSpeed avec lsphp74, VM Proxmox chez un hébergeur français.
À quoi ressemble une attaque DDoS L7 sur WordPress
Une attaque DDoS volumétrique classique sature votre bande passante. Une attaque DDoS de couche 7 fait autre chose : elle envoie peu de trafic réseau mais demande à chaque fois un cycle WordPress complet. C’est beaucoup moins visible et beaucoup plus efficace.
Voici la signature relevée dans les logs d’accès pendant la première vague.
| Indicateur | Valeur mesurée |
|---|---|
| Requêtes sur la minute de pic | 10 713 |
| Connexions TCP établies | 3 875 |
Requêtes ciblant / | 492 sur un échantillon de 500 |
| Chargement d’assets par les clients | aucun |
| User-agents | Chrome 147 à 150 en rotation |
| Origines | Tor (environ 40 %), Chine, Vietnam, Brésil, Inde, Maroc |
Le détail qui tranche : aucun de ces clients ne charge le CSS, le JS ou les images après la page. Un navigateur réel demande la page puis ses 50 à 80 dépendances. Ces clients demandaient la page, rien d’autre, et recommençaient. À partir de là il n’y a plus de doute sur la nature du trafic.
Côté serveur, le résultat s’est traduit par du thrashing mémoire pur : vmstat affichait 167 Mo/s en swap-in et 209 Mo/s en swap-out, avec 24 à 32 % d’iowait. Les processus PHP ne parvenaient même plus à allouer le buffer d’écriture de leur réponse. Ce n’est pas WordPress qui était lent, c’est le noyau qui n’arrivait plus à donner de la RAM à qui que ce soit.
Pourquoi l’anti-DDoS de l’hébergeur n’a rien vu pendant deux heures
C’est le point que nous voulons faire retenir en priorité, parce qu’il change la façon de concevoir sa supervision.
Le pic applicatif a eu lieu à 11 h 03. Le mail de détection de l’hébergeur est arrivé à 12 h 56, presque deux heures plus tard, quand l’attaque a basculé sur un volume réseau suffisant pour déclencher les seuils, 2 400 Mbps sur le graphe. Sur six heures, l’infrastructure de mitigation a rejeté 5,51 millions de paquets et nettoyé 11,16 Gb.
Sauf que pendant les deux heures les plus critiques, aucune alerte. Le flood L7 initial était trop léger en bande passante pour être vu par une protection réseau, et suffisamment lourd en CPU et en RAM pour mettre la VM à genoux. Une protection anti-DDoS incluse chez votre hébergeur vous protège de la couche 3 et de la couche 4. Elle ne vous protège pas de 10 000 requêtes HTTP légitimement formées sur votre page d’accueil.
Si vous n’avez qu’une seule alerte à mettre en place demain matin, mettez-la sur le taux d’occupation du swap. C’est le premier indicateur qui a bougé, bien avant tout le reste.
fail2ban derrière Docker : 970 bans sans le moindre effet
Première contre-mesure déployée par l’équipe : fail2ban, avec un filtre qui compte les requêtes non-assets par IP. La logique est simple et elle fonctionne bien contre ce profil de DDoS applicatif. Un visiteur réel charge la page puis ses assets, donc on exclut les assets du comptage et on isole précisément le pattern d’attaque.
[Definition]
failregex = ^<ADDR> .*"[^"]*" d{3} S+
ignoreregex = ^.*"[A-Z]+ [^" ]*.(?:css|js|mjs|jpe?g|png|gif|svg|webp|avif|woff2?|ttf|eot|ico|map)(?:?[^" ]*)? [^"]*" d{3}Résultat immédiat : 147 904 tentatives détectées, 493 IP bannies dans la première passe, puis environ 1 500 cumulées. Les connexions établies sont retombées de 3 875 à 511 puis à 333. Bien.
Sauf que le soir même, une fois BunkerWeb installé en conteneur, des IP déjà bannies continuaient à atteindre le serveur web. fail2ban-client annonçait 999 bans actifs. ipset était rempli. Et pourtant, rien ne bloquait.
La cause est une propriété de Docker que peu de gens ont en tête : le trafic destiné à un conteneur ne traverse jamais la chaîne INPUT. Il passe par nat PREROUTING en DNAT puis par FORWARD. Or fail2ban insère ses règles dans INPUT par défaut.
iptables -L INPUT -n | grep f2b
REJECT all -- 0.0.0.0/0 0.0.0.0/0 match-set f2b-web-flood src <- présent
iptables -L DOCKER-USER -n
Chain DOCKER-USER (1 references)
RETURN all -- 0.0.0.0/0 0.0.0.0/0 <- videUn fail2ban parfaitement fonctionnel, des milliers de bans comptabilisés, zéro effet réel. Il faut écrire une action qui vise DOCKER-USER.
[Definition]
actionstart = ipset create <ipmset> hash:ip timeout <default-ipsettime> -exist
iptables -I DOCKER-USER -m set --match-set <ipmset> src -j <blocktype>
actionstop = iptables -D DOCKER-USER -m set --match-set <ipmset> src -j <blocktype>
ipset flush <ipmset>
ipset destroy <ipmset>
actionban = ipset add <ipmset> <ip> timeout <ipsettime> -exist
actionunban = ipset del <ipmset> <ip> -exist
[Init]
ipmset = f2b-<name>
blocktype = DROPDeux détails ont coûté une demi-heure chacun. Un fichier d’action déposé uniquement en .local n’est pas chargé par fail2ban, il faut un .conf. Et le paramètre family transmet inet4 alors qu’ipset attend inet, ce qui produit un The set with the given name does not exist répété mille fois. Retirez-le.
Dernier piège du même ordre, découvert au redémarrage suivant : fail2ban doit démarrer après Docker, sinon Docker recrée ses chaînes iptables au boot et efface les règles. Un After=docker.service dans l’unité systemd règle la question.
L’architecture retenue : BunkerWeb devant OpenLiteSpeed
Le choix a été de placer BunkerWeb en reverse proxy sur les ports 80 et 443, et de faire redescendre OpenLiteSpeed en backend. La question qui se pose alors est celle du réseau Docker.
network_mode: host a été écarté : conflit direct sur le port 8080 avec LiteSpeed, et surtout perte de la résolution DNS entre conteneurs, dont dépendent database, redis et bunkerweb dans le compose fourni par le projet.
La solution retenue fait écouter LiteSpeed sur la gateway docker0.
Internet -> ens18 -> [conteneur bunkerweb] -> 172.17.0.1:8080 -> [LiteSpeed sur l'hôte]L’adresse 172.17.0.1 est joignable depuis les conteneurs et n’est jamais joignable depuis Internet, puisque le trafic public arrive sur l’interface physique. Aucune modification du docker-compose.yml n’est nécessaire.
Côté WordPress, il manque une chose, et sans elle vous obtenez une boucle de redirection infinie : BunkerWeb reçoit du HTTPS, transmet en HTTP, WordPress constate qu’il n’est pas en HTTPS et redirige vers HTTPS. Le bloc à ajouter dans wp-config.php :
/* Reverse proxy BunkerWeb : détection HTTPS */
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}Pensez aussi à useIpInProxyHeader 2 et à un logFormat commençant par %{X-Forwarded-For}i côté LiteSpeed, sinon tous vos logs d’accès contiennent la même IP interne et votre fail2ban ne sert plus à rien.
Le challenge JavaScript, c’est ce qui a arrêté l’attaque
USE_ANTIBOT="javascript" est la ligne qui a changé la situation, et c’est elle qui a réellement stoppé le DDoS. Avant d’accéder à quoi que ce soit, le client est redirigé vers /challenge et doit résoudre un calcul en JavaScript. Le paramètre next= mémorise la destination initiale, donc l’utilisateur atterrit bien sur la page qu’il demandait.
[ACCESS] antibot returned status 0 : displaying challenge to client
client: 89.x.x.x, request: "GET /challenge?next=%2Ffr%2Fjob%2F..."
status=200 bytes=15001 req_time=0.003 ua="Mozilla/5.0 ... Firefox/142.0"Trois millisecondes pour un vrai Firefox. Au même moment, les bots qui martelaient des URL aléatoires étaient rejetés en amont par la limite de connexions. Les humains passent, les bots non. C’est aussi simple que ça, et c’est ce qui a fait la différence là où le rate limiting seul n’y arrivait pas.
Un bémol honnête : le challenge n’est pas gratuit. Pour chaque bot il faut générer une page de 15 Ko, ouvrir une session et interroger Redis. C’est plus cher qu’un rejet sec. Sur un flood très large, l’antibot devient lui-même une charge.
Les cinq pièges qui ont failli tout casser
Aucun de ces problèmes ne venait de l’attaquant. Ils venaient de notre configuration.
1. Le rate limiting bannit vos vrais visiteurs
Une page Avada avec vidéos embarquées génère 50 à 80 requêtes, dont plusieurs MP4 de 15 à 46 Mo. À LIMIT_REQ_RATE=10r/s, un visiteur légitime dépasse la limite en ouvrant votre page d’accueil. Console navigateur : 90 erreurs 429 sur les JS Avada et les vidéos. Il a fallu monter à 60r/s puis 100r/s, et passer LIMIT_CONN_MAX_HTTP2 de 10 à 30.
2. Ne comptez jamais les 429 dans BAD_BEHAVIOR
Le cercle vicieux le plus élégant de tout l’incident. Le visiteur dépasse le rate limit, reçoit des 429, le module Bad Behavior compte ces 429 comme un comportement d’attaquant, et bannit le visiteur pour 24 heures. Un 429 est une réponse de votre WAF, pas un signal d’attaque. Sortez-le de BAD_BEHAVIOR_STATUS_CODES.
3. BunkerWeb bascule silencieusement sur son ancienne config
Un vhost avait à la fois un REVERSE_PROXY_HOST sur / et un REDIRECT_TO. Nginx a refusé la config générée avec un duplicate location "/". BunkerWeb n’a pas planté : il a repris sa dernière configuration valide, sans les certificats et sans les bons vhosts. Trois symptômes apparemment sans rapport, certificat auto-signé, redirection vers /setup, 502 sur www, pour une seule cause. Vérifiez systématiquement après chaque modification.
docker compose logs bunkerweb | grep -iE "emerg|failing over|reloaded"4. docker compose restart ne relit pas env_file
Trente minutes perdues à chercher pourquoi USE_ANTIBOT n’apparaissait pas dans l’environnement du conteneur. Les variables sont figées à la création. Il faut docker compose up -d --force-recreate.
5. WORKER_CONNECTIONS sans WORKER_RLIMIT_NOFILE
À 08 h 48 le lendemain matin : 1024 worker_connections are not enough. Effet domino immédiat, nginx n’a plus de connexion libre pour joindre Redis, donc plus moyen de vérifier les bans, donc des 429 généralisés y compris sur les visiteurs légitimes. La valeur par défaut de WORKER_RLIMIT_NOFILE est 2048 et chaque connexion consomme au moins un descripteur de fichier, souvent deux. Monter l’un sans l’autre fait planter nginx.
WORKER_CONNECTIONS=8192
WORKER_RLIMIT_NOFILE=32768
DATASTORE_MEMORY_SIZE=256m
CACHESTORE_MEMORY_SIZE=256mBonus, pour ceux qui seraient tentés : un redis-cli FLUSHALL détruit des structures internes de BunkerWeb et le laisse dans un état où /challenge renvoie une page blanche sans le moindre log. Passez par bwcli unban.
La limite structurelle d’un WAF hébergé sur la machine cible
Le site a été remis en ligne, mais nous n’allons pas prétendre que le problème était réglé pour autant. Au plus fort de l’attaque, BunkerWeb consommait 5,1 Go de RAM et 497 % de CPU, et les temps de réponse mesurés dans le WAF montaient à 51 secondes avec upstream="-" sur toutes les lignes. Autrement dit : plus aucune requête n’atteignait LiteSpeed. Le goulot d’étranglement s’était déplacé, il n’avait pas disparu.
C’est mécanique. Chaque connexion, même rejetée en 429, doit être acceptée, déchiffrée en TLS, analysée par les quelques centaines de règles OWASP CRS, puis fermée. Tout ça avec les ressources de la machine que vous essayez de protéger.
| Approche | Filtrage | Ce que ça coûte |
|---|---|---|
| Plugin de sécurité WordPress | après PHP | quasiment inutile en L7, PHP a déjà tourné |
| WAF sur la VM cible | avant PHP, sur vos ressources | fonctionne, jusqu’à saturation du WAF |
| WAF sur une VM dédiée | avant PHP, ressources séparées | le trafic atteint toujours votre infra |
| Reverse proxy CDN en amont | avant votre infrastructure | changement de nameservers |
Un WAF sur la machine cible est la bonne réponse à un DDoS de quelques milliers de requêtes par minute et la mauvaise réponse à un flood massif. Pour du volume, le filtrage doit avoir lieu avant que le trafic n’atteigne votre réseau.
Le durcissement qui a suivi
Le DDoS a surtout servi de révélateur. Il a mis au jour des problèmes qui traînaient depuis des années sur cette machine et qui n’avaient rien à voir avec l’attaquant.
Le cache LiteSpeed était désactivé, enableCache 0, depuis la mise en production. Chaque visite déclenchait un cycle WordPress complet. Il y avait par ailleurs 3,3 Go de fichiers de cache datant de mars 2023 dans cachedata, résidus d’une configuration antérieure.
Le logLevel était sur DEBUG en production. Pendant l’attaque, ça a généré des fichiers de 10 à 210 Mo toutes les trente secondes, environ 40 Go au total, et une saturation I/O qui a aggravé le reste.
Cinq sites tournaient sous le même utilisateur système. Quand un site est compromis, les quatre autres le sont aussi. La séparation des utilisateurs par site est fastidieuse à mettre en place et vous remerciera un jour.
Et les dumps SQL étaient dans le webroot. 719 Mo de base de données accessibles en HTTP. Ça n’a rien de spécifique à cet incident, mais allez vérifier maintenant si vous n’avez pas la même chose quelque part.
Ce que nous retenons
Trois choses, dans l’ordre où elles nous ont le plus servi.
La supervision d’abord. La détection du DDoS est venue d’un graphe CPU, pas d’une alerte. Le swap avait commencé à monter bien avant que quiconque s’en aperçoive, et c’était l’indicateur à surveiller.
Les outils ensuite. fail2ban et BunkerWeb fonctionnent tous les deux très bien, et tous les deux échouent silencieusement quand on les configure mal. Aucun des deux ne vous dit « je suis actif mais je ne bloque rien ». Il faut aller vérifier soi-même, dans iptables -L DOCKER-USER et dans les logs de rechargement.
Le calibrage enfin, et c’est le plus contre-intuitif. Nous avons passé plus de temps à empêcher le WAF de bannir de vrais visiteurs qu’à bloquer l’attaquant. Un site Avada chargé en vidéos ressemble beaucoup à un bot agressif vu depuis un compteur de requêtes par seconde. Si vous déployez ce genre de protection, testez d’abord avec votre propre navigateur, console ouverte, avant de laisser le trafic réel arriver dessus.
Nous exploitons et sécurisons des parcs WordPress à forte audience. Si ce scénario vous parle, nous réalisons des audits de sécurité et de performance sur infrastructures WordPress, dans le prolongement de nos prestations DevOps et cloud. Parlons-en.
Questions fréquentes
Comment protéger un site WordPress contre une attaque DDoS ?
Le filtrage doit intervenir avant PHP, dans le serveur web ou dans un reverse proxy. Un WAF placé devant le site, couplé à un challenge JavaScript, arrête un flood applicatif de quelques milliers de requêtes par minute. Au-delà, il faut filtrer en amont de l’infrastructure, via un reverse proxy CDN.
L’anti-DDoS de l’hébergeur suffit-il à protéger un site ?
Non, il protège des couches 3 et 4, pas d’un flood applicatif. Lors de l’incident d’août 2026, le pic à 10 713 requêtes par minute est passé inaperçu pendant près de deux heures : trop léger en bande passante pour déclencher les seuils réseau, largement suffisant pour saturer la mémoire du serveur.
Que faire quand un site est en train de subir une attaque DDoS ?
Identifier d’abord la nature du trafic en comparant le ratio pages sur ressources dans les logs : un navigateur réel charge la page puis ses dépendances, un bot demande la page et repart. Bloquer ensuite par IP en attendant, puis poser un filtrage en amont de PHP. Un blocage seul ne tient pas face à des IP tournantes.
Combien coûte la mise en place d’une protection anti-DDoS ?
Le déploiement d’un WAF devant un site existant se compte en une soirée d’intervention, suivie de deux à trois jours de réglage fin. En régie, comptez un TJM de 400 à 1 200 € HT. L’exploitation ensuite relève de l’infogérance, de 70 à 500 € HT par mois et par serveur selon le niveau de service.
Un plugin de sécurité WordPress suffit-il contre une attaque DDoS ?
Non. Un plugin s’exécute après le chargement de WordPress et de PHP : le coût de la requête est déjà payé au moment où la décision de blocage est prise. Contre un flood applicatif, seul un filtrage en amont de PHP a un effet.
Comment savoir si un site subit une attaque ou un simple pic de trafic ?
Regardez si les clients chargent les ressources après la page. Un navigateur réel demande le HTML puis le CSS, le JS et les images. Un bot demande le HTML et repart. Un calcul du ratio page sur ressources par IP dans les logs d’accès répond en trente secondes.
Combien de temps faut-il pour remettre un site en ligne après une attaque ?
Sur l’incident documenté ici, 48 heures ont été nécessaires entre le début de l’attaque et le rétablissement complet du service. La partie longue n’est pas le déploiement de la protection, mais son calibrage : éviter que le filtrage ne bannisse les vrais visiteurs prend plus de temps que bloquer l’attaquant.
