D'abord, assurez-vous qu'il s'agit bien d'une attaque
L'erreur la plus coûteuse pendant une panne, c'est de traiter le mauvais problème. « Le site est lent » est un symptôme que partagent un déluge authentique, un déploiement qui a embarqué une requête sans index, un cron qui s'est mis à vider la base de données à la même minute chaque heure, un crawler qui a découvert votre recherche à facettes, un lien qui a atteint la page d'accueil d'un site important, et un disque qui s'est rempli. Chacun de ces cas est identique vu d'un navigateur, et les réponses à y apporter s'excluent mutuellement : vous ne voulez pas limiter le débit de vrais clients parce qu'une migration a oublié un index.
Trois questions permettent de les distinguer en moins d'une minute. Le volume de trafic est-il vraiment anormal, ou le trafic est-il normal et le serveur lent ? Un déluge se traduit par un changement brutal du nombre de paquets ou de requêtes par seconde ; un mauvais déploiement se traduit par un volume de requêtes normal et un temps de réponse effondré. Quelque chose a-t-il changé de votre côté dans la dernière heure ? Vérifiez le journal de déploiement avant le pare-feu — les incidents auto-infligés sont bien plus nombreux que les attaques sur les petites infrastructures. La charge est-elle répartie sur tout le site, ou concentrée sur un seul chemin ? Les vrais déluges sont généralement indiscriminés ou visent la page d'accueil ; un endpoint coûteux martelé par une centaine de clients relève davantage de l'abus que du DDoS, et sa correction coûte bien moins cher.
Répondez à ces questions, puis continuez. Le reste de ce guide part du principe que la réponse était : le volume est anormal, rien n'a changé de votre côté, et la machine est en train de couler.
SP·02Le triage des cinq premières minutes
Il n'existe que deux modes de défaillance qui comptent, et ils appellent des réponses opposées. Soit le tuyau est plein — les paquets arrivent plus vite que votre liaison montante ou votre noyau ne peuvent les traiter, et votre serveur perd du trafic avant même que votre logiciel ne le voie — soit le tuyau va bien et c'est l'application qui est épuisée, parce que des requêtes parfaitement formées arrivent plus vite qu'elle ne peut y répondre. Les confondre gaspille l'heure : régler nginx ne fait rien contre un lien saturé, et acheter plus de bande passante ne fait rien contre un déluge de requêtes.
Distinguez-les en comparant deux chiffres. Regardez à la fois les compteurs d'interface et la répartition du CPU. Si les octets rx sont collés près du débit de votre port, si les compteurs dropped ou overrun grimpent, et si le temps part dans les interruptions logicielles plutôt que dans votre application, le déluge se situe en couche 3 ou 4 et c'est un problème de capacité. Si la bande passante est banale mais que le pool de workers est saturé, que les connexions font la queue, et que le journal d'accès est plein de requêtes qui ont l'air plausibles, c'est de la couche 7 et c'est un problème de filtrage.
Affinez ensuite la classification du cas couche 3/4, parce que les sous-types se comportent différemment. Un déluge SYN se traduit par des dizaines de milliers de sockets à moitié ouverts en SYN-RECV ; le noyau gère bien cela une fois les syncookies activés. Un déluge UDP ou par amplification se traduit par un volume entrant énorme sur des ports que vous n'écoutez même pas — réflexions DNS, NTP, memcached, CLDAP — et rien de ce que vous faites tourner ne peut y changer quoi que ce soit, parce que les dégâts sont faits au moment où les paquets atteignent votre carte réseau. Un déluge de fragmentation ou de paquets bruts se traduit par un nombre élevé de paquets par seconde avec une bande passante modeste, ce qui affame le CPU plutôt que le lien. Notez lequel vous avez avant de toucher à un fichier de configuration.
Ce que vous ne pouvez pas corriger depuis l'intérieur de la machine
C'est la partie que la plupart des articles sautent, et c'est elle qui décide si votre heure est productive ou non. Une règle de pare-feu sur la cible ne sauve pas une liaison montante saturée. Votre DROP iptables s'exécute sur la machine au bout du tuyau — le paquet a déjà traversé le lien de transit, déjà consommé la bande passante que vous payez, et déjà pris la place du paquet d'un vrai utilisateur. Le rejeter localement protège votre application du gaspillage de cycles, ce qui a une vraie valeur, mais ne protège absolument pas votre bande passante.
Le plafond honnête d'un serveur unique est à peu près le plus petit des deux chiffres suivants : la vitesse du port sur lequel il est connecté, et le nombre de paquets par seconde que son CPU peut classifier. Un port à 1 Gbps est plein à 1 Gbps quelle que soit l'élégance de votre jeu de règles, et un déluge modeste de petits paquets peut épuiser plusieurs cœurs rien qu'en traitement des interruptions bien avant que le chiffre de bande passante ne paraisse alarmant. Passé ce point, la seule chose qui aide est un équipement plus en amont, qui dispose de plus de capacité que l'attaque et qui rejette le trafic avant même qu'il n'atteigne votre lien. C'est exactement ce qu'est le scrubbing, et c'est pourquoi chaque machine de notre flotte se trouve derrière 1.5 Tbps de mitigation permanente plutôt que derrière un plus gros pare-feu.
Le corollaire compte tout autant : si vous n'avez aucune protection en amont, la réponse de votre hébergeur face à une grosse attaque volumétrique est de placer votre adresse en null-route, parce que l'alternative est de dégrader tous les autres clients sur ce lien. Ce n'est pas de la malveillance, c'est de l'arithmétique — et cela signifie que l'attaquant gagne en vous rendant coûteux plutôt qu'en cassant quoi que ce soit. Savoir à l'avance si la mitigation est incluse dans votre plan ou si c'est une option payante que vous n'avez jamais activée est une vérification de cinq minutes qu'il vaut mieux faire aujourd'hui que pendant l'incident. La nôtre est incluse sur tous les plans, ce qui est le seul arrangement qui aide à 3 heures du matin.
SP·04Couche 7 : le déluge qui ressemble à du trafic
Un déluge de couche applicative est plus difficile, parce que chaque requête prise individuellement est légitime. Un déluge HTTP bien construit termine la poignée de main TCP, négocie le TLS, envoie un GET / valide avec un user agent plausible, et lit la réponse. Aucun paquet n'y est malformé. Ce qui vous tue, c'est l'arithmétique : une requête ne coûte presque rien à l'attaquant pour l'envoyer, et vous coûte une requête en base de données, un rendu de template et une centaine de millisecondes d'un worker qui, pendant ce temps, ne sert plus personne d'autre.
Les indices se trouvent dans votre propre journal d'accès, et ils sont généralement évidents une fois qu'on les cherche au lieu de simplement les regarder. Les chaînes de requête anti-cache — des milliers de requêtes vers /?1234567, chacune étant une URL unique qui déjoue tous les caches que vous possédez — sont la signature la plus fréquente, et de loin. Une distribution de user agents sans longue traîne : le trafic réel est un mélange désordonné de centaines de versions de navigateurs, alors qu'un déluge se limite souvent à trois chaînes répétées un million de fois, ou à une seule qu'aucun humain réel n'utilise. Un champ referrer identique partout. Des requêtes qui ignorent entièrement vos ressources statiques — un vrai navigateur récupère le CSS, les polices et les images après le HTML ; un client de déluge demande le HTML et repart. Et une distribution des sources trop plate : un botnet réparti sur dix mille adresses résidentielles envoyant chacune deux requêtes par seconde ressemble à du succès jusqu'à ce qu'on remarque que le taux par adresse est étrangement uniforme.
Il existe ensuite la variante qui n'a presque besoin d'aucun trafic : l'attaque lente. Quelques centaines de connexions qui s'ouvrent, envoient un en-tête toutes les vingt secondes et ne se terminent jamais, occuperont tous les workers que vous avez pendant que votre graphique de bande passante reste plat. Le correctif n'est pas une limite de débit — le débit de requêtes est minuscule — ce sont des délais d'expiration agressifs sur les en-têtes et le corps, ce pourquoi ils figurent dans le premier bloc de configuration ci-dessous plutôt qu'en pensée après coup.
SP·05Quatre contrôles, par ordre d'effet par minute
Sous pression, faites d'abord ce qui a le plus fort effet de levier. L'ordre ci-dessous n'est pas arbitraire ; il est à peu près décroissant selon la charge que chaque mesure décharge par minute d'attention que vous y consacrez.
- Servez quelque chose de bon marché. Un micro-cache de trente secondes placé devant votre application transforme mille requêtes identiques par seconde en un seul hit sur l'origine et 999 lectures mémoire. C'est le levier le plus puissant sur presque tous les déluges HTTP, cela coûte un seul bloc de directives, et pour du trafic anonyme c'est presque toujours sans risque. Ajoutez
proxy_cache_lockpour qu'un cache miss n'envoie pas une ruée massive vers le backend. - Plafonnez la concurrence et raccourcissez les délais d'expiration.
limit_connpar adresse, combiné à des délais d'expiration stricts sur les en-têtes, le corps et le keepalive, tue net les attaques lentes et empêche un client isolé de monopoliser votre pool de workers. C'est le contrôle qui coûte le moins aux vrais utilisateurs. - Limitez le débit, avec un burst.
limit_reqavec un burst raisonnable est précis mais plus long à régler, et c'est celui qui génère des faux positifs si vous le configurez dans la panique plutôt qu'à partir de votre propre référence. Vous devez connaître votre nombre normal de requêtes par seconde et par client avant de pouvoir choisir un chiffre — c'est pourquoi le post-mortem à la fin de ce guide compte plus qu'il n'y paraît. - Bloquez, étroitement et à contrecœur. Rejeter des réseaux spécifiques fonctionne quand les sources sont concentrées et ne sert à rien quand elles ne le sont pas. Cela vieillit également mal : chaque blocage ajouté pendant un incident est peut-être un client que vous refusez silencieusement trois mois plus tard. Utilisez un set avec un timeout pour que les règles expirent d'elles-mêmes.
Remarquez ce qui ne figure pas sur cette liste : bannir des adresses IP une par une à la main, redémarrer le serveur web à répétition, et désactiver le pare-feu pour « voir si ça aide ». La première est trop lente pour peser face à une source distribuée, la deuxième jette toutes les connexions chaudes que vous aviez, et la troisième est la façon dont un incident se transforme en compromission.
SP·06Ne le combattez pas depuis la machine qui détient vos données
Chaque contrôle ci-dessus vaut davantage lorsqu'il tourne ailleurs que sur la machine qui détient votre base de données. Si votre edge est un nœud séparé, le déluge se termine sur une machine dont tout le métier consiste à terminer les déluges : elle met en cache, limite le débit et rejette en connaissant la vraie adresse du client, et l'origine ne voit jamais que le petit reliquat filtré, via un tunnel privé. Quand l'edge tombe, vous le remplacez en 15 min et ne perdez rien, parce qu'il n'y a rien dessus. Quand l'origine tombe, vous avez une panne et une restauration.
Cette séparation referme également le contournement qui rend la plupart des mitigations purement décoratives. Si l'origine possède encore un écouteur public, un attaquant qui trouve son adresse — via le DNS passif, une entrée de transparence des certificats, un enregistrement MX ou un aperçu de lien — peut viser au-delà de tous les contrôles que vous avez configurés et frapper directement l'application. C'est assez fréquent pour que ce soit l'état par défaut à supposer pour tout site « protégé », jusqu'à preuve du contraire. Construire la version qui tient est un guide à part entière : un reverse proxy offshore sans aucun écouteur public sur l'origine.
Une mise en garde à propos du milieu d'un incident : c'est de l'architecture, pas des premiers secours. Monter un edge, déplacer le DNS et reconstruire un tunnel en pleine attaque est un travail de deux heures fait à la va-vite sous pression, et le changement de DNS à lui seul ne prendra effet qu'après le délai indiqué par votre TTL. Si vous l'avez déjà, utilisez-le. Si vous ne l'avez pas, passez le cap de l'heure avec les contrôles dont vous disposez, et construisez-le pendant la semaine calme qui suit — ce qui est précisément le moment où personne ne le fait.
SP·07Le jour où c'est vous le réflecteur, pas la cible
Il existe une seconde version de cet incident, où votre serveur n'est pas la victime et où personne ne vous prévient. Un résolveur ouvert, un démon NTP exposé, un memcached non authentifié sur une interface publique, un répondeur SSDP ou CLDAP à l'intérieur d'un conteneur — chacun d'eux répond à une petite requête usurpée par une réponse bien plus volumineuse, dirigée vers quelqu'un d'autre. De votre côté, les symptômes sont inversés : la bande passante sortante est élevée, l'entrante est modeste, votre application va bien, et le premier signal réel est une notification d'abus ou un port suspendu.
Cette vérification prend une minute et a sa place dans le même runbook, parce que c'est la même commande que vous avez déjà lancée pendant le triage. ss -tulpn ne devrait rien lister de lié à une adresse publique que vous n'y avez pas mis délibérément, et les services UDP méritent une suspicion particulière, car ce sont eux qui amplifient. Un résolveur récursif doit être lié à localhost ou à une adresse de tunnel ; memcached et Redis ne doivent jamais être joignables depuis internet, en aucun cas ; et tout conteneur publiant un port avec -p 0.0.0.0: vient de percer un trou dans le pare-feu que vous avez configuré, parce que Docker écrit ses propres règles avant les vôtres. Ce dernier point surprend tout le monde, à chaque fois.
Le même schéma couvre les déluges sortants provenant d'une machine déjà compromise, ce qui est l'autre raison pour laquelle un hébergeur place soudain une adresse en null-route. Si votre graphique sortant est élevé et que votre application est inactive, arrêtez de lire de la configuration et commencez à vérifier les processus — c'est une intrusion, pas un problème de capacité, et la bonne réponse consiste à reconstruire la machine à partir d'une sauvegarde saine plutôt qu'à la filtrer.
SP·08Une fois que ça s'arrête : le post-mortem et le kit permanent
Les attaques s'arrêtent. En général l'attaquant se lasse, parfois la mitigation la rend inutile, et il arrive que ce soit un abonnement de booter à durée fixe qui a simplement expiré. La tentation, à ce moment-là, est de tout laisser exactement en l'état et d'aller se coucher — c'est ainsi qu'une limite de débit temporaire devient un 429 permanent, oublié et silencieux pour tout un pays, neuf mois plus tard. Passez vingt minutes à boucler le sujet pendant que c'est encore frais.
Trois artefacts méritent d'être produits. Une référence : votre nombre normal de requêtes par seconde, votre taux normal par client, votre bande passante normale au pic. Sans ces chiffres, chaque limite que vous fixez pendant le prochain incident est un pari, et la moitié d'entre elles se tromperont dans le sens qui pénalise les clients. Une liste de retour arrière : tout ce que vous avez changé, avec une date et une raison, pour que la configuration d'urgence ne devienne pas silencieusement la configuration permanente. Un runbook long de quatre commandes, qui vit quelque part que vous pouvez atteindre quand le site est en panne — pas sur le serveur, et pas seulement dans votre tête.
Comblez ensuite les deux lacunes structurelles, parce que c'est là que cette couche se situe dans la pile. La mitigation est ce qui absorbe le volume, un edge est ce qui tient le déluge à l'écart de vos données, et la machine derrière eux doit quand même être correctement durcie et disposer de sauvegardes hors site restaurables, parce que l'incident suivant n'en sera peut-être pas un du tout. Un serveur qui survit à un DDoS et qui perd son disque un mois plus tard n'a jamais été résilient — il a eu de la chance, deux fois.
SP·09Étape par étape
-
01
Confirmez avant de changer quoi que ce soit
Obtenez une image honnête de la machine avant de toucher à un fichier de configuration. Vous cherchez un changement brutal dans les paquets ou les requêtes, et où part le temps CPU — une application privée de CPU par des interruptions logicielles est un incident très différent d'une application qui attend une base de données.
# is the box alive, and where is the time going? uptime # load average against your core count vmstat 1 5 # 'in' and 'cs' high, 'id' near zero = packet work mpstat -P ALL 1 3 # %soft pinned on one core = interrupt saturation # is the pipe full, or just busy? ip -s link show eth0 # rx bytes, and the errors/dropped counters ethtool eth0 | grep -i speed # requests per second from your own log, minute by minute tail -n 20000 /var/log/nginx/access.log \ | awk -F'[][]' '{print $2}' | cut -d: -f2-4 | uniq -c | tail -5Avant de conclure qu'il s'agit d'une attaque, vérifiez votre propre journal de déploiement et votre table cron. Les pannes auto-infligées sont plus fréquentes que les déluges sur un VPS unique, et elles se ressemblent de l'extérieur.
-
02
Mesurez la forme du trafic en soixante secondes
Classifiez-le maintenant. Trois questions : combien de sources distinctes, quel protocole et quel état, et — s'il s'agit de HTTP — quels chemins et quels agents. Les réponses déterminent quel contrôle vous allez utiliser, et elles prennent environ une minute à recueillir.
# top source addresses on the wire right now timeout 20 tcpdump -nn -i eth0 -c 20000 2>/dev/null \ | awk '{print $3}' | rev | cut -d. -f2- | rev \ | sort | uniq -c | sort -rn | head -20 # TCP state census: a wall of SYN-RECV is a SYN flood ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn # layer 7: talkers, paths, agents over the last 50k requests L=/var/log/nginx/access.log tail -n 50000 $L | awk '{print $1}' | sort | uniq -c | sort -rn | head -20 tail -n 50000 $L | awk '{print $7}' | sort | uniq -c | sort -rn | head -20 tail -n 50000 $L | cut -d'"' -f6 | sort | uniq -c | sort -rn | head -10Confrontez le résultat aux indices : des chaînes de requête uniques sur un seul chemin, une liste de user agents sans longue traîne, aucune requête vers vos ressources statiques, ou un taux par adresse étrangement uniforme. Si la bande passante est élevée sur des ports que vous n'écoutez pas, arrêtez-vous ici — c'est un déluge volumétrique, et l'étape six est la seule qui compte.
-
03
Servez quelque chose de bon marché, et plafonnez les connexions
Priorité au plus fort effet de levier. Un micro-cache de trente secondes réduit un déluge de requêtes anonymes identiques à un seul hit sur l'origine, et des délais d'expiration stricts tuent les attaques lentes qu'une limite de débit ne peut pas voir. Placez les deux dans le bloc
http, puis faites un reload plutôt qu'un restart pour conserver vos connexions chaudes.# /etc/nginx/nginx.conf — http block limit_conn_zone $binary_remote_addr zone=perip:10m; limit_req_zone $binary_remote_addr zone=flood:20m rate=10r/s; client_header_timeout 10s; client_body_timeout 10s; send_timeout 10s; keepalive_timeout 20s; reset_timedout_connection on; proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=hot:64m max_size=2g inactive=10m use_temp_path=off;# the server block — cap concurrency, serve the cached copy limit_conn perip 20; location / { proxy_cache hot; proxy_cache_valid 200 301 302 30s; proxy_cache_lock on; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; add_header X-Cache $upstream_cache_status; proxy_pass http://127.0.0.1:8080; }nginx -t && systemctl reload nginx curl -sI https://example.com/ | grep -i x-cache # want: HIT on the second call
-
04
Limitez le débit du vrai client, pas celui de votre propre proxy
Si quoi que ce soit se trouve devant nginx, chaque requête arrive depuis une seule adresse, et une limite par adresse va freiner le proxy au lieu de l'attaquant — ou carrément le bannir et mettre le site hors ligne pendant que vous essayez de le défendre. Ne faites confiance à l'en-tête forwarded que depuis l'adresse du proxy, jamais depuis internet, puis appliquez la limite.
# /etc/nginx/conf.d/realip.conf — the tunnel or edge address only set_real_ip_from 10.66.0.1; real_ip_header X-Forwarded-For; real_ip_recursive off;
# burst absorbs bursty humans; nodelay keeps the page fast for them location / { limit_req zone=flood burst=20 nodelay; limit_req_status 429; } # the expensive paths get a much tighter bucket of their own location ~ ^/(search|login|register|api/) { limit_req zone=flood burst=5; limit_req_status 429; }Observez ensuite ce que vous venez de faire :
tail -f /var/log/nginx/error.log | grep limiting. Si les adresses limitées ressemblent à vos clients, le taux est trop bas — augmentez-le. Une limite qui bloque de vrais utilisateurs est une panne que vous vous êtes infligée vous-même. -
05
Bloquez étroitement, et donnez une expiration à chaque blocage
Cela ne vaut la peine que si l'étape deux a montré des sources concentrées. Utilisez un set, pas mille règles — une recherche
ipsetse fait en temps constant, alors qu'une longue chaîneiptablesest parcourue pour chaque paquet et finit par devenir son propre déni de service. Donnez un timeout à chaque entrée pour que l'urgence d'aujourd'hui ne devienne pas la liste de blocage silencieuse de l'an prochain.ipset create flood hash:net timeout 3600 -exist iptables -I INPUT -m set --match-set flood src -j DROP # feed it from the census: /24s you actually verified, not guesses for n in 203.0.113.0/24 198.51.100.0/24; do ipset add flood $n -exist; done # SYN flood: let the kernel do the part it is good at sysctl -w net.ipv4.tcp_syncookies=1 sysctl -w net.ipv4.tcp_max_syn_backlog=8192 sysctl -w net.core.somaxconn=8192 ipset list flood | head -20 # keep a copy of this for the post-mortem
Résistez à l'envie de geo-bloquer tout un pays, sauf si vous pouvez nommer les clients que vous excluez. Et ne faites jamais
ufw disablepour tester une théorie : une machine sans pare-feu sous attaque active, c'est comment un incident de bande passante se transforme en compromission. -
06
Escaladez vers la couche qui peut réellement l'absorber
Si les compteurs d'interface indiquent que le lien est saturé, ou que les drops grimpent pendant que votre CPU est inactif, vous avez atteint le plafond de ce que vous pouvez faire sur le serveur. Confirmez cette lecture, puis escaladez au lieu de continuer à régler des paramètres.
# drops in the stack itself — second column is 'dropped' awk '{print strtonum("0x" $2)}' /proc/net/softnet_stat | paste -sd+ | bc # interface-level loss against link speed ip -s link show eth0 | sed -n '3,6p'Avec un scrubbing en amont en place, il n'y a généralement rien à faire : la détection est permanente et le déluge est rejeté dans le réseau avant même d'atteindre votre port — sur notre flotte, ce sont 1.5 Tbps de capacité placés devant chaque plan, si bien que l'incident n'est souvent visible qu'après coup, sous forme de graphique. Si l'attaque est un déluge HTTP bien formé plutôt qu'un déluge volumétrique, c'est le cas d'usage d'un bouclier L7, parce que les déluges de requêtes sont indiscernables des utilisateurs au niveau du paquet et doivent être jugés plus haut. Si vous n'avez aucune mitigation du tout, vos options réalistes sont de vous placer derrière un edge qui en a une, ou d'attendre — et de planifier la première option avant la prochaine attaque.
-
07
Bouclez la boucle : vérifiez, revenez en arrière, puis notez-le
Vérifiez depuis l'extérieur de la machine, pas depuis un shell dessus. Défaites ensuite délibérément les mesures d'urgence, gardez celles qui étaient de toute façon une bonne idée, et enregistrez les chiffres pour que le prochain incident parte d'une référence plutôt que d'un pari.
# from somewhere else entirely: is the site healthy for a normal user? curl -s -o /dev/null -w 'code=%{http_code} ttfb=%{time_starttransfer}s\n' \ https://example.com/ # did you leave a limit that is biting real people? grep -c 'limiting requests' /var/log/nginx/error.log # what is still blocked, and when does it expire? ipset list flood | head -30Gardez le cache, les délais d'expiration et les syncookies — ce sont des améliorations permanentes. Ramenez les limites de débit agressives à votre référence mesurée plus une marge, et laissez les entrées ipset expirer d'elles-mêmes. Écrivez ensuite les quatre commandes des étapes un et deux dans un runbook stocké quelque part qui n'est pas ce serveur, à côté de votre nombre normal de requêtes par seconde et de votre bande passante de pic normale. La prochaine attaque sera un incident plus court, uniquement parce que ces deux chiffres existent.


