Il y a quelques jours, en consultant le htop d’un serveur en production, je suis tombé sur une charge anormalement élevée pour le trafic réel que le serveur était censé recevoir. Un coup d’oeil rapide aux logs nginx et le coupable était là : un défilé interminable de bots, scrapers IA et crawlers SEO martelant le site à toute heure. Aucun trafic humain, juste des robots faisant leurs affaires.
Le serveur en question joue le rôle de proxy inverse devant plusieurs sites web, ce qui lui confère une position privilégiée : tout le trafic passe par lui avant d’atteindre les backends. C’est donc l’endroit idéal pour installer une première ligne de défense et filtrer les indésirables avant qu’ils ne touchent à quoi que ce soit d’autre.
L’idée est simple : beaucoup de ces bots s’identifient encore (honnêtement) via leur User-Agent. GPTBot, ClaudeBot, Bytespider, AhrefsBot, SemrushBot et consorts laissent leur signature dans chaque requête. Alors pourquoi ne pas utiliser exactement cela comme filtre et leur renvoyer un sobre 403 en pleine face ?
Particularité : c’est sur Debian
Avant d’entrer dans le vif du sujet, une note importante car elle influe sur où vont les fichiers. Cette configuration tourne sur Debian, et sur Debian (comme sur les dérivées type Ubuntu) le nginx.conf installé par défaut inclut déjà une ligne comme celle-ci dans le bloc http :
include /etc/nginx/conf.d/*.conf;
Cela signifie que tout fichier .conf déposé dans /etc/nginx/conf.d/ est chargé automatiquement. Très pratique pour garder la configuration bien découpée plutôt que de tout entasser au même endroit. Si vous travaillez sur une autre distro ou sur FreeBSD, vérifiez que cet include existe (ou ajoutez-le), car sinon le fichier ne se chargera pas par magie.
Le fichier de filtrage
On crée le fichier /etc/nginx/conf.d/bad-bots.conf avec le contenu suivant. Il utilise la directive map pour créer une variable $blocked_agent qui vaudra 1 si le User-Agent correspond à l’un des motifs, et 0 sinon :
map $http_user_agent $blocked_agent {
default 0;
# --- Scrapers IA / LLM ---
"~*openai" 1; # OpenAI
"~*GPTBot" 1; # OpenAI
"~*ChatGPT-User" 1; # OpenAI (plugins)
"~*OAI-SearchBot" 1; # OpenAI
"~*ClaudeBot" 1; # Anthropic
"~*Claude-Web" 1; # Anthropic
"~*anthropic-ai" 1; # Anthropic
"~*Google-Extended" 1; # Google AI (training Bard/Gemini)
"~*Applebot-Extended" 1; # Apple AI
"~*Bytespider" 1; # ByteDance / TikTok (tres agressif)
"~*CCBot" 1; # Common Crawl
"~*PerplexityBot" 1; # Perplexity
"~*Perplexity-User" 1; # Perplexity
"~*Amazonbot" 1; # Amazon
"~*FacebookBot" 1; # Meta (entrainement IA)
"~*meta-externalagent" 1; # Meta external agent
"~*meta-externalfetcher" 1; # Meta external fetcher
"~*Diffbot" 1;
"~*Omgilibot" 1;
"~*Omgili" 1;
"~*Applebot" 1;
"~*ImagesiftBot" 1;
"~*cohere-ai" 1;
"~*cohere-training-data-crawler" 1;
"~*Timpibot" 1;
"~*YouBot" 1;
"~*BacklinksExtendedBot" 1;
"~*YandexBot" 1;
"~*DataForSeoBot" 1;
"~*facebookexternalhit" 1;
# --- Crawlers SEO / marketing bruyants ---
"~*AhrefsBot" 1;
"~*SemrushBot" 1;
"~*DotBot" 1; # Moz
"~*MJ12bot" 1; # Majestic
"~*BLEXBot" 1;
"~*MegaIndex" 1;
"~*SeznamBot" 1;
"~*serpstatbot" 1;
"~*PetalBot" 1; # Huawei
"~*ZoominfoBot" 1;
"~*Barkrowler" 1;
"~*IbouBot" 1;
"~*AliyunSecBot" 1;
"~*AwarioBot" 1;
"~*Qwantbot" 1;
# --- Outils de telechargement / scraping generiques ---
"~*wget" 1;
"~*curl" 1; # attention : bloque curl legitime, a retirer si vous l'utilisez
"~*python-requests" 1;
"~*python-urllib" 1;
"~*Scrapy" 1;
"~*Go-http-client" 1;
"~*libwww-perl" 1;
"~*HTTrack" 1;
"~*WebCopier" 1;
"~*WebReaper" 1;
"~*Nutch" 1;
# --- Vide ou suspect ---
"" 1; # User-Agent vide
}
Quelques détails sur la syntaxe pour comprendre ce qui se passe :
Le ~* devant chaque motif indique une expression régulière insensible à la casse, donc GPTBot, gptbot ou GPTBOT seront tous interceptés. Le default 0; est la clé : par défaut tout passe, et on ne marque comme bloqué (1) que ce qui correspond explicitement. C’est une approche par liste noire, plus permissive que l’inverse, mais facile à maintenir.
La dernière entrée, "" 1;, bloque les requêtes qui arrivent sans User-Agent. Un navigateur ou client légitime en envoie presque toujours un, donc un User-Agent vide est généralement le signe de quelque chose d’automatisé et mal conçu.
Activer le filtre sur chaque vhost
La directive map seule ne bloque rien : elle définit simplement la variable. Pour qu’elle entre en action, il faut l’utiliser là où on le souhaite. Dans le server { ... } (ou le location concerné) de chaque vhost où vous voulez appliquer le filtre, ajoutez :
if ($blocked_agent) {
return 403;
}
Et c’est tout. Si le User-Agent de la requête a déclenché le map, nginx répond avec un 403 Forbidden et ne se donne même pas la peine de transmettre la requête au backend. L’avantage de l’avoir sur le proxy inverse, c’est que vous protégez d’un coup tous les sites derrière lui, sans toucher à chaque application une par une.
Variante : return 444 (fermer sans répondre)
Le 403 est correct et sémantiquement honnête, mais il a un petit coût : nginx génère et envoie une réponse d’erreur au client. Si vous voulez dépenser le minimum de ressources possible avec ces bots, nginx propose un code non standard très pratique, le 444, qui ferme la connexion immédiatement sans envoyer aucune réponse :
if ($blocked_agent) {
return 444;
}
Le bot se retrouve avec la connexion coupée net — pas de corps, pas d’en-têtes, rien. Cela consomme moins de bande passante et moins de CPU que retourner une page d’erreur, et donne moins d’informations à celui qui est de l’autre côté. Je préfère cette variante pour les bots clairement automatisés ; je réserve le 403 aux cas où je veux que le « interdit » soit explicitement consigné.
Avant de recharger, on vérifie toujours que la configuration est valide :
# nginx -t
Et si tout est correct, on recharge sans interrompre le service :
# systemctl reload nginx
Journaliser les blocages pour savoir ce qu’on attrape
Bloquer c’est bien, mais à l’aveugle c’est inconfortable : comment savoir si on filtre ce qu’il faut, ou si on casse quelque chose de légitime sans s’en rendre compte ? L’astuce consiste à envoyer les blocages dans leur propre log pour pouvoir les auditer sans polluer l’access_log général.
nginx permet de conditionner l’écriture d’un access_log avec if=, ce qui tombe à pic puisqu’on dispose déjà de la variable $blocked_agent. On définit d’abord un format de log personnalisé dans le bloc http :
log_format blockedhosts '$remote_addr - [$time_local] "$request" $status "$http_user_agent"';
Puis dans le vhost, juste à côté du if, on écrit dans ce log uniquement quand la requête a été marquée comme bloquée :
if ($blocked_agent) {
return 444;
}
access_log /var/log/nginx/blocked_agents.log blockedhosts if=$blocked_agent;
La clé est le if=$blocked_agent à la fin : nginx n’écrira une ligne dans blocked_agents.log que lorsque cette variable vaut 1. Le trafic légitime ne touche pas à ce fichier. Vous obtenez ainsi un registre propre et dédié de tout ce que vous bloquez.
À partir de là, un simple coup d’oeil en dit long. Pour voir quels User-Agents sont les plus insistants :
# awk -F'"' '{print $6}' /var/log/nginx/blocked_agents.log | sort | uniq -c | sort -rn | head
C’est une excellente façon d’affiner la liste en continu : si vous voyez tomber quelque chose qui ne devrait pas, retirez-le du map ; et si vous découvrez un nouveau bot faisant des dégâts, ajoutez-le.
Astuce : n’oubliez pas d’ajouter
blocked_agents.logà votrelogrotate, sinon avec le temps il peut grossir plus que prévu.
Revoyez et adaptez la liste à vos besoins
C’est la liste qui fonctionne pour moi, mais ne la copiez pas aveuglément. Parcourez-la attentivement et adaptez-la à votre cas :
Si vous utilisez curl pour vos propres healthchecks, votre monitoring ou vos déploiements, ce "~*curl" 1; va vous jouer un mauvais tour en bloquant des requêtes légitimes. Idem avec wget, python-requests ou Go-http-client si vous avez des scripts internes qui appellent le site. La liste est la vôtre : ajoutez, retirez et testez.
Le bilan : -50 % de charge
Le résultat sur ce serveur de production ? Après application du filtre, la charge du serveur a diminué de plus de 50 %. Oui, plus de la moitié de ce qu’il traitait était, purement et simplement, du bruit automatisé qui n’apportait strictement rien. De quoi réfléchir à la proportion du trafic internet actuel qui n’est que des robots parlant à d’autres robots.
Ce n’est pas infaillible (important)
Voici l’avertissement de rigueur, parce qu’il vaut mieux avoir des attentes réalistes. Filtrer par User-Agent est une première ligne de défense bon marché, rapide et étonnamment efficace contre les bots qui s’identifient honnêtement… mais rien de plus.
Le User-Agent est un en-tête HTTP que le client envoie et qui peut être falsifié en une seconde. N’importe quel scraper ayant le moindre intérêt à contourner le filtre n’a qu’à envoyer un User-Agent de navigateur normal pour passer sans problème. Autrement dit : cela arrête les bots « polis » et les outils génériques, mais ne stoppera pas quelqu’un qui veut vraiment entrer.
Pour une protection sérieuse contre le trafic malveillant, le scraping agressif ou les attaques, il faut monter d’un cran et envisager un WAF (Web Application Firewall) ou des solutions de mitigation plus complètes. Quelques exemples à considérer :
Cloudflare (avec son mode de blocage des bots et scrapers IA), Anubis (un proof-of-work léger conçu précisément contre les crawlers IA, très en vogue en ce moment), BitNinja ou Sucuri, pour n’en citer que quelques-uns. Chacun a son approche, ses avantages et ses inconvénients.
En attendant, ce filtre nginx est un excellent premier échelon : gratuit, sans dépendances externes, facile à maintenir, et il élimine une quantité de bruit impressionnante. Et parfois, diviser par deux la charge d’un serveur avec quatre lignes de configuration, c’est exactement ce dont vous aviez besoin.