# Robots d'IA avec nginx - robots.txt, blocage par User-Agent, plages IP officielles et débit

> Configuration nginx testée pour refuser les robots d'entraînement, laisser passer les vrais robots de recherche IA, limiter leur débit et les journaliser.

Source : https://iabig.com/fr/plateformes/robots-ia-nginx/ — Guides CC BY 4.0, extraits de code MIT

Avec nginx, tout se règle dans la configuration du serveur : on peut servir robots.txt, refuser une requête selon son User-Agent, vérifier qu'un robot vient bien des adresses de son éditeur, limiter son débit et l'inscrire dans un journal séparé. Ce guide assemble ces briques en une configuration complète, organisée autour d'une politique simple : refuser l'entraînement, laisser passer les robots de recherche de ChatGPT, Claude et Perplexity, et renvoyer une erreur 403 à ceux qui empruntent leur nom sans venir de leurs plages IP. Nous avons validé cette configuration avec `nginx -t` puis par des requêtes réelles sur nginx 1.30.6, en version officielle pour Windows ; les chemins Linux indiqués (`/etc/nginx`, `/var/log/nginx`) sont ceux des paquets Debian et Ubuntu et sont à adapter à votre distribution.

## robots.txt d'abord

Le blocage côté serveur ne remplace pas robots.txt : c'est le fichier que lisent les robots qui respectent vos choix, et le seul moyen d'exprimer un refus d'entraînement auprès de Google (`Google-Extended`) ou d'Apple (`Applebot-Extended`), qui n'envoient aucune requête sous ces noms. Placez le fichier à la racine du site et assurez-vous qu'il réponde toujours en 200, quel que soit le robot :

```nginx
location = /robots.txt {
    # Servi à tous, y compris aux robots refusés ailleurs
    try_files $uri =404;
    access_log off;   # facultatif : ne pas encombrer le journal principal
}
```

La RFC 9309 prévoit qu'un robots.txt injoignable en erreur 5xx vaut interdiction complète, et un robots.txt refusé en 403 empêche le robot d'y lire votre refus. La configuration qui suit exclut donc `/robots.txt` de tous les blocages.

## La configuration du bloc http

Ce fichier se place dans le contexte `http`, par exemple dans `/etc/nginx/conf.d/`, inclus par défaut sur Debian et Ubuntu. Chaque `map` ne coûte rien tant que sa variable n'est pas utilisée : nginx l'évalue à la demande.

```nginx
# /etc/nginx/conf.d/robots-ia.conf  (contexte http)

# 1. Robot d'IA annoncé par le User-Agent.
#    Les expressions sont testées dans l'ordre ; ~* = insensible à la casse.
map $http_user_agent $robot_ia {
    default                 "";
    "~*GPTBot"              GPTBot;
    "~*OAI-SearchBot"       OAI-SearchBot;
    "~*ChatGPT-User"        ChatGPT-User;
    "~*ClaudeBot"           ClaudeBot;
    "~*Claude-SearchBot"    Claude-SearchBot;
    "~*Claude-User"         Claude-User;
    "~*PerplexityBot"       PerplexityBot;
    "~*Perplexity-User"     Perplexity-User;
    "~*CCBot"               CCBot;
    "~*meta-externalagent"  meta-externalagent;
    "~*Bytespider"          Bytespider;
}

# 2. Robots refusés côté serveur (ici : entraînement ; à adapter)
map $robot_ia $robot_refuse {
    default             0;
    GPTBot              1;
    ClaudeBot           1;
    CCBot               1;
    meta-externalagent  1;
    Bytespider          1;
}

# 3. Éditeur revendiqué, pour les robots dont les plages IP sont publiées
map $robot_ia $editeur_annonce {
    default           "";
    GPTBot            openai;
    OAI-SearchBot     openai;
    ChatGPT-User      openai;
    ClaudeBot         anthropic;
    Claude-SearchBot  anthropic;
    Claude-User       anthropic;
    PerplexityBot     perplexity;
    Perplexity-User   perplexity;
}

# 4. Éditeur réel d'après l'adresse IP (fichier généré par le script plus bas)
geo $editeur_ip {
    default none;
    include /etc/nginx/robots-ia-ip.conf;
}

# 5. 1 = robot annoncé, mais IP hors des plages de son éditeur
map "$editeur_annonce:$editeur_ip" $robot_usurpe {
    default                  1;
    "~^:"                    0;   # aucun robot vérifiable annoncé
    "openai:openai"          0;
    "anthropic:anthropic"    0;
    "perplexity:perplexity"  0;
}

# 6. Décision finale. robots.txt reste toujours lisible.
map "$robot_refuse$robot_usurpe:$uri" $refus_ia {
    default                  0;
    "~^..:/robots\.txt$"     0;   # doit rester la première expression
    "~^1"                    1;   # robot refusé par principe
    "~^01"                   1;   # robot usurpé
}

# 7. Limitation de débit : seuls les robots d'IA ont une clé.
#    Une clé vide n'est pas comptée.
map $robot_ia $cle_limite_ia {
    ""       "";
    default  $binary_remote_addr;
}
limit_req_zone $cle_limite_ia zone=robots_ia:10m rate=1r/s;

# 8. Format du journal dédié
log_format robots_ia '$remote_addr [$time_local] "$request" $status '
                     'robot=$robot_ia ip=$editeur_ip refus=$refus_ia "$http_user_agent"';
```

Quelques points de lecture. Dans un `map`, la documentation de nginx indique que la recherche s'arrête au premier résultat, les chaînes exactes passant avant les expressions régulières, elles-mêmes testées dans l'ordre du fichier. Le motif `~*GPTBot` ne correspond pas à `ChatGPT-User` (qui contient « GPT-U », pas « GPTBot ») et `~*ClaudeBot` ne correspond pas à `Claude-SearchBot` : l'ordre ci-dessus est sans ambiguïté. Le module `geo` accepte les préfixes IPv4 et IPv6 en notation CIDR. Enfin, `limit_req_zone` ignore les requêtes dont la clé est vide : les visiteurs ordinaires ne sont pas limités.

## Le bloc server

```nginx
server {
    listen 443 ssl;
    server_name www.example.com;
    # ssl_certificate et ssl_certificate_key : inchangés, omis ici
    root /var/www/example;

    # Un access_log déclaré ici remplace ceux hérités de http :
    # on redéclare le journal principal, puis le journal dédié.
    access_log /var/log/nginx/access.log combined;
    access_log /var/log/nginx/robots-ia.log robots_ia if=$robot_ia;

    # 1 requête/s par adresse pour les robots d'IA, rafale de 5 tolérée
    limit_req zone=robots_ia burst=5 nodelay;
    limit_req_status 429;

    # Refus (robots d'entraînement et usurpateurs), sauf pour robots.txt
    if ($refus_ia) {
        return 403;
    }

    location = /robots.txt {
        try_files $uri =404;
    }

    location / {
        try_files $uri $uri/ =404;
    }
}
```

Les directives `if` et `return` du module rewrite sont admises dans le contexte `server` ; placées là, elles s'appliquent à toutes les URL, quel que soit le `location` choisi ensuite. Le paramètre `if=` de `access_log` n'écrit la ligne que si la variable n'est ni vide ni égale à « 0 » : le journal `robots-ia.log` ne contient que les requêtes d'un robot d'IA annoncé, acceptées ou refusées. Le code 429 de `limit_req_status` (503 par défaut) indique clairement au robot qu'il doit ralentir.

Si le site est derrière un CDN ou un répartiteur de charge, `$remote_addr` contient l'adresse de cet intermédiaire : configurez d'abord le module `realip` (`set_real_ip_from`, `real_ip_header`), sinon tous les vrais robots seront classés comme usurpateurs.

## Mettre à jour les plages IP officielles

OpenAI, Anthropic et Perplexity publient leurs plages dans des fichiers JSON de même format : un tableau `prefixes` dont chaque élément contient `ipv4Prefix` ou `ipv6Prefix`. Le script suivant, que nous avons exécuté contre les fichiers réels le 25 octobre 2026 (311 préfixes obtenus), produit le fichier inclus par `geo` et ne recharge nginx que si la configuration reste valide.

```bash
#!/usr/bin/env bash
# /usr/local/sbin/maj-robots-ia-nginx.sh - dépend de curl et jq
set -euo pipefail
sortie=/etc/nginx/robots-ia-ip.conf
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT

ajoute() {  # $1 = éditeur, $2 = URL du fichier JSON officiel
  # -L : perplexity.com redirige vers perplexity.ai
  curl -fsSL --max-time 30 "$2" \
    | jq -er --arg e "$1" '.prefixes[] | (.ipv4Prefix // .ipv6Prefix) | select(. != null) | "\(.) \($e);"' >> "$tmp"
}
ajoute openai     https://openai.com/gptbot.json
ajoute openai     https://openai.com/searchbot.json
ajoute openai     https://openai.com/chatgpt-user.json
ajoute anthropic  https://claude.com/crawling/bots.json
ajoute perplexity https://www.perplexity.com/perplexitybot.json
ajoute perplexity https://www.perplexity.com/perplexity-user.json

# Garde-fou : une liste anormalement courte signale un problème de téléchargement
[ "$(wc -l < "$tmp")" -ge 10 ] || { echo "liste trop courte, abandon" >&2; exit 1; }

sort -u "$tmp" > "$sortie.new"
cp -p "$sortie" "$sortie.bak" 2>/dev/null || true
mv "$sortie.new" "$sortie"

if ! nginx -t; then
  echo "configuration refusée, retour à l'ancienne liste" >&2
  [ -f "$sortie.bak" ] && mv "$sortie.bak" "$sortie"
  exit 1
fi
systemctl reload nginx
```

```text
# /etc/cron.d/maj-robots-ia : tous les jours à 4 h 17
17 4 * * * root /usr/local/sbin/maj-robots-ia-nginx.sh
```

Avec `set -e`, un téléchargement en échec arrête le script et l'ancienne liste reste en place. Avant le premier lancement, créez un fichier vide (`touch /etc/nginx/robots-ia-ip.conf`) pour que `nginx -t` réussisse. Pour vérifier à la main une adresse relevée dans vos journaux, le [vérificateur d'IP de robots IABIG](/fr/outils/verifier-ip-robot/) interroge les mêmes listes.

## X-Robots-Tag, si besoin

```nginx
location ~* \.pdf$ {
    add_header X-Robots-Tag "noindex" always;
}
```

Google documente cet en-tête ; les pages d'OpenAI, d'Anthropic et de Perplexity consultées pour ce guide ne le mentionnent pas. Attention à l'héritage : les directives `add_header` d'un niveau ne sont héritées que si le niveau courant n'en déclare aucune. Un `add_header` dans ce `location` fait disparaître ceux du bloc `server`.

## Tester

```bash
nginx -t && systemctl reload nginx

# Navigateur : 200
curl -s -o /dev/null -w '%{http_code}\n' https://www.example.com/
# Robot d'entraînement : 403, mais 200 sur robots.txt
curl -s -o /dev/null -w '%{http_code}\n' -A "compatible; GPTBot/1.4" https://www.example.com/
curl -s -o /dev/null -w '%{http_code}\n' -A "compatible; GPTBot/1.4" https://www.example.com/robots.txt
# Robot de recherche annoncé depuis votre poste : 403 (IP hors des plages)
curl -s -o /dev/null -w '%{http_code}\n' -A "compatible; OAI-SearchBot/1.4" https://www.example.com/

tail -f /var/log/nginx/robots-ia.log
```

Ces commandes sont indicatives : curl reproduit le User-Agent, pas l'adresse du robot. Le dernier test le montre bien : depuis votre poste, un faux `OAI-SearchBot` est refusé, ce qui est le comportement attendu. Lors de nos essais, en déclarant l'adresse locale comme plage d'OpenAI, `OAI-SearchBot` obtenait 200, puis 429 au-delà de la rafale ; `GPTBot` recevait 403 sur `/` et 200 sur `/robots.txt`. Le vrai contrôle se fait ensuite dans `robots-ia.log`, où la colonne `ip=` indique l'éditeur reconnu pour chaque requête.

## Pièges à éviter

- **Le User-Agent seul ne prouve rien.** Le refus par nom arrête les robots honnêtes, que robots.txt arrêtait déjà ; les usurpateurs ne sont repérés que par la comparaison avec les plages IP, et seulement pour les éditeurs qui en publient.
- **Une liste périmée bloque de vrais robots.** Les plages changent ; surveillez la date de génération des fichiers et les refus `refus=1` portant sur des robots de recherche.
- **Une expression trop large refuse tout le monde.** `~*bot` attraperait Googlebot et Bingbot.
- **Bloquer les plages d'un éditeur pour refuser l'entraînement.** Anthropic indique que cette méthode n'est pas fiable, car elle empêche aussi la lecture de robots.txt.

## Revenir en arrière

Commentez la ligne `if ($refus_ia) { return 403; }` et la directive `limit_req`, puis `nginx -t` et `systemctl reload nginx` : le trafic n'est plus filtré mais reste journalisé. Pour tout retirer, supprimez `/etc/nginx/conf.d/robots-ia.conf`, les lignes ajoutées au bloc `server` et la tâche cron.

## Sources officielles

- nginx, module map : <https://nginx.org/en/docs/http/ngx_http_map_module.html>
- nginx, module geo : <https://nginx.org/en/docs/http/ngx_http_geo_module.html>
- nginx, module limit_req : <https://nginx.org/en/docs/http/ngx_http_limit_req_module.html>
- nginx, module log (access_log, log_format) : <https://nginx.org/en/docs/http/ngx_http_log_module.html>
- nginx, module headers (add_header) : <https://nginx.org/en/docs/http/ngx_http_headers_module.html>
- nginx, module realip : <https://nginx.org/en/docs/http/ngx_http_realip_module.html>
- RFC 9309, Robots Exclusion Protocol : <https://www.rfc-editor.org/rfc/rfc9309>
- OpenAI, Overview of OpenAI Crawlers : <https://developers.openai.com/api/docs/bots>
- Anthropic, Does Anthropic crawl data from the web : <https://support.claude.com/en/articles/8896518>
- Perplexity, Perplexity Crawlers : <https://docs.perplexity.ai/docs/resources/perplexity-crawlers>
- Google, Robots meta tag et X-Robots-Tag : <https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag>
