# Vérifier qu'un robot GPTBot, ClaudeBot ou PerplexityBot est authentique - plages IP et DNS inverse

> Confirmer qu'une requête GPTBot, ClaudeBot ou PerplexityBot vient bien de son éditeur grâce aux plages IP officielles ou au DNS, puis bloquer les imitateurs.

Source : https://iabig.com/fr/robots-ia/verifier-authenticite-gptbot-claudebot-perplexitybot/ — Guides CC BY 4.0, extraits de code MIT

N'importe quel script peut envoyer l'en-tête `User-Agent: GPTBot`. Dans les journaux d'un site, une partie des visites attribuées à des robots d'IA provient en réalité de collecteurs qui empruntent leur nom pour passer les filtres, ou simplement pour se fondre dans le trafic. Common Crawl le signale d'ailleurs sur sa propre page : des robots se font passer pour CCBot. La seule façon de trancher est de rapprocher l'adresse IP de la requête de ce que publie l'éditeur. Ce guide indique la méthode documentée pour chaque robot, donne des commandes prêtes à l'emploi, puis montre comment bloquer les imitateurs avec nginx et fail2ban.

## Quelle méthode pour quel robot

Les éditeurs n'utilisent pas tous la même méthode. La plupart publient un fichier JSON de plages IP ; Google, Apple et Common Crawl documentent en plus la vérification par DNS inverse puis direct.

| Robot | Méthode documentée | Source de vérité |
|---|---|---|
| `GPTBot` | Plages IP | `https://openai.com/gptbot.json` |
| `OAI-SearchBot` | Plages IP | `https://openai.com/searchbot.json` |
| `ChatGPT-User` | Plages IP | `https://openai.com/chatgpt-user.json` |
| `ClaudeBot`, `Claude-SearchBot`, `Claude-User` | Plages IP (une liste unique pour tous les robots Anthropic) | `https://claude.com/crawling/bots.json` |
| `PerplexityBot` | Plages IP | `https://www.perplexity.com/perplexitybot.json` |
| `Perplexity-User` | Plages IP | `https://www.perplexity.com/perplexity-user.json` |
| `Googlebot` et fetchers Google | DNS inverse + direct, ou plages IP | `googlebot.com`, `google.com`, `googleusercontent.com` |
| `Applebot` | DNS inverse ou plages IP | `*.applebot.apple.com`, `https://search.developer.apple.com/applebot.json` |
| `CCBot` | Plages IP, DNS inverse + direct en IPv4 | `*.crawl.commoncrawl.org`, `https://index.commoncrawl.org/ccbot.json` |

OpenAI, Anthropic et Perplexity ne documentent pas de nom d'hôte en DNS inverse : pour ces robots, ne fondez pas une décision sur le DNS, utilisez les fichiers JSON. Leur format est le même partout : un objet `prefixes` dont chaque élément contient `ipv4Prefix` ou `ipv6Prefix` en notation CIDR, accompagné d'un horodatage `creationTime`.

```text
{
  "creationTime": "...",
  "prefixes": [
    { "ipv4Prefix": "192.0.2.0/24" },
    { "ipv6Prefix": "2001:db8::/32" }
  ]
}
```

Les plages changent : OpenAI a par exemple régénéré `chatgpt-user.json` le 20 octobre 2026. Téléchargez-les régulièrement plutôt que de les recopier une fois pour toutes.

## Extraire les plages avec curl et jq

```bash
# Liste des préfixes CIDR publiés pour GPTBot
curl -fsSL https://openai.com/gptbot.json | jq -r '.prefixes[] | .ipv4Prefix // .ipv6Prefix'

# Date de génération du fichier
curl -fsSL https://claude.com/crawling/bots.json | jq -r '.creationTime'

# Nombre de préfixes par fichier
for u in https://openai.com/gptbot.json \
         https://openai.com/searchbot.json \
         https://claude.com/crawling/bots.json \
         https://www.perplexity.com/perplexitybot.json; do
  printf '%5d  %s\n' "$(curl -fsSL "$u" | jq '.prefixes | length')" "$u"
done
```

jq ne sait pas tester l'appartenance d'une adresse à un réseau CIDR. Pour cette étape, Python et son module standard `ipaddress` sont plus fiables qu'une expression régulière.

## Tester une adresse IP avec Python

Le script suivant télécharge les listes officielles et indique à quel robot appartient une adresse. Il n'utilise que la bibliothèque standard.

```python
#!/usr/bin/env python3
"""Indique si une adresse IP appartient aux plages publiées d'un robot d'IA."""
import ipaddress
import json
import sys
import urllib.request

SOURCES = {
    "GPTBot": "https://openai.com/gptbot.json",
    "OAI-SearchBot": "https://openai.com/searchbot.json",
    "ChatGPT-User": "https://openai.com/chatgpt-user.json",
    "Anthropic (ClaudeBot, Claude-SearchBot, Claude-User)": "https://claude.com/crawling/bots.json",
    "PerplexityBot": "https://www.perplexity.com/perplexitybot.json",
    "Perplexity-User": "https://www.perplexity.com/perplexity-user.json",
}


def prefixes(url):
    req = urllib.request.Request(url, headers={"User-Agent": "verif-ip/1.0"})
    with urllib.request.urlopen(req, timeout=15) as resp:
        data = json.load(resp)
    for p in data.get("prefixes", []):
        cidr = p.get("ipv4Prefix") or p.get("ipv6Prefix")
        if cidr:
            yield ipaddress.ip_network(cidr, strict=False)


def main():
    if len(sys.argv) != 2:
        sys.exit("usage : verifier_bot.py <adresse IP>")
    ip = ipaddress.ip_address(sys.argv[1])
    trouve = False
    for nom, url in SOURCES.items():
        try:
            reseaux = list(prefixes(url))
        except Exception as exc:  # réseau, JSON invalide…
            print(f"[erreur] {nom} : {exc}", file=sys.stderr)
            continue
        for net in reseaux:
            if ip.version == net.version and ip in net:
                print(f"{ip} appartient à {nom} ({net})")
                trouve = True
    if not trouve:
        print(f"{ip} n'appartient à aucune plage publiée vérifiée")
    sys.exit(0 if trouve else 1)


if __name__ == "__main__":
    main()
```

```bash
python3 verifier_bot.py 20.171.206.5
# 20.171.206.5 appartient à GPTBot (20.171.206.0/24)   (résultat au 25 octobre 2026)

# Passer en revue toutes les IP qui se présentent comme GPTBot dans un journal
grep -i 'GPTBot' /var/log/nginx/access.log | awk '{print $1}' | sort -u |
  while read -r ip; do python3 verifier_bot.py "$ip" >/dev/null || echo "SUSPECT $ip"; done
```

Le code de sortie (0 si l'adresse est trouvée, 1 sinon) permet de l'utiliser dans un script. Pour un gros volume, chargez les listes une seule fois plutôt que de relancer le script pour chaque adresse.

Attention à la granularité : la liste d'Anthropic couvre tous ses robots sans les distinguer. Une IP qui y figure prouve que la requête vient d'Anthropic, pas qu'il s'agit de ClaudeBot plutôt que de Claude-User.

## DNS inverse puis direct, quand c'est la méthode documentée

Pour Google, Apple et Common Crawl, la vérification se fait en deux temps : résolution inverse de l'IP vers un nom d'hôte, contrôle du domaine, puis résolution directe de ce nom qui doit redonner la même IP. La seconde étape est indispensable, car le propriétaire d'une adresse IP choisit librement son enregistrement PTR.

```bash
# Exemple publié par Google
host 66.249.66.1
# 1.66.249.66.in-addr.arpa domain name pointer crawl-66-249-66-1.googlebot.com.
host crawl-66-249-66-1.googlebot.com
# crawl-66-249-66-1.googlebot.com has address 66.249.66.1

# Apple : le nom doit se terminer par .applebot.apple.com
host 17.58.101.179

# Common Crawl : le nom doit se terminer par .crawl.commoncrawl.org (IPv4 seulement)
dig +short -x 18.97.14.84
dig +short 18-97-14-84.crawl.commoncrawl.org A
```

La même logique en Python, pour automatiser :

```python
#!/usr/bin/env python3
"""Vérification DNS inverse puis directe (FCrDNS)."""
import ipaddress
import socket
import sys

DOMAINES = {
    "googlebot": (".googlebot.com", ".google.com", ".googleusercontent.com"),
    "applebot": (".applebot.apple.com",),
    "ccbot": (".crawl.commoncrawl.org",),
}


def fcrdns(ip, suffixes):
    try:
        nom = socket.gethostbyaddr(ip)[0].rstrip(".")
    except OSError:
        return None, False
    if not nom.endswith(suffixes):
        return nom, False
    try:
        adresses = {ipaddress.ip_address(ai[4][0].split("%")[0])
                    for ai in socket.getaddrinfo(nom, None)}
    except OSError:
        return nom, False
    return nom, ipaddress.ip_address(ip) in adresses


if __name__ == "__main__":
    if len(sys.argv) != 3 or sys.argv[1] not in DOMAINES:
        sys.exit("usage : fcrdns.py googlebot|applebot|ccbot <adresse IP>")
    nom, ok = fcrdns(sys.argv[2], DOMAINES[sys.argv[1]])
    print(f"{sys.argv[2]} -> {nom} : {'authentique' if ok else 'NON vérifié'}")
    sys.exit(0 if ok else 1)
```

Le contrôle porte sur le suffixe précédé d'un point : `googlebot.com.example.net` ou `faux-googlebot.com` sont rejetés. Google précise que les fetchers déclenchés par un utilisateur résolvent vers `google.com` ou `gae.googleusercontent.com` selon les cas ; un nom en `googleusercontent.com` peut donc correspondre à un service hébergé sur Google Cloud et non à Google lui-même.

## Bloquer les imitateurs avec nginx

L'approche la plus robuste consiste à laisser nginx comparer, pour chaque requête, le robot annoncé et l'éditeur propriétaire de l'IP. On génère d'abord un fichier de plages pour le module `geo` :

```bash
#!/usr/bin/env bash
# /usr/local/sbin/maj-ip-robots-ia.sh - à lancer chaque jour (cron ou timer systemd)
set -euo pipefail
sortie=/etc/nginx/robots-ia-ip.conf
tmp=$(mktemp)
ajoute() {  # $1 = éditeur, $2 = URL du JSON officiel
  curl -fsSL "$2" | jq -r --arg v "$1" '.prefixes[] | (.ipv4Prefix // .ipv6Prefix) + " " + $v + ";"' >> "$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
[ -s "$tmp" ] || { echo "liste vide, abandon" >&2; exit 1; }
sort -u "$tmp" > "$sortie.new" && rm -f "$tmp"
mv "$sortie.new" "$sortie"
nginx -t && systemctl reload nginx
```

Avec `set -e`, un téléchargement en échec interrompt le script et l'ancienne liste reste en place. La configuration nginx, dans le bloc `http` :

```nginx
# Éditeur propriétaire de l'IP d'après les listes officielles
geo $ip_editeur_ia {
    default none;
    include /etc/nginx/robots-ia-ip.conf;
}

# Éditeur revendiqué par le User-Agent
map $http_user_agent $ua_editeur_ia {
    default                                   "";
    "~*GPTBot|OAI-SearchBot|ChatGPT-User"     openai;
    "~*ClaudeBot|Claude-SearchBot|Claude-User" anthropic;
    "~*PerplexityBot|Perplexity-User"         perplexity;
}

# 1 = robot annoncé mais IP hors des plages de son éditeur
map "$ua_editeur_ia:$ip_editeur_ia" $robot_usurpe {
    default                1;
    "~^:"                  0;   # aucun robot d'IA annoncé
    "openai:openai"        0;
    "anthropic:anthropic"  0;
    "perplexity:perplexity" 0;
}

log_format robots_usurpes '$remote_addr [$time_local] "$request" "$http_user_agent"';
```

Puis dans le bloc `server` du site :

```nginx
# Une directive access_log dans server remplace celles héritées de http :
# on redéclare donc le journal principal.
access_log /var/log/nginx/access.log combined;
access_log /var/log/nginx/robots-usurpes.log robots_usurpes if=$robot_usurpe;
if ($robot_usurpe) {
    return 403;
}
```

Les vrais robots passent, les imitateurs reçoivent une 403 et sont consignés dans un journal dédié. Pour bloquer une adresse précise sans attendre, la directive classique suffit : `deny 203.0.113.7;` dans le bloc `server`, ou un fichier inclus regroupant ces lignes.

Si votre site est derrière un CDN ou un proxy, `$remote_addr` contient l'adresse du proxy. Configurez d'abord le module `realip` (`set_real_ip_from` et `real_ip_header`) pour retrouver l'IP du client, sinon les vrais robots seront classés comme imitateurs.

## Bannir les récidivistes avec fail2ban

Le journal `robots-usurpes.log` ne contient que des requêtes déjà identifiées comme frauduleuses : fail2ban peut s'en servir pour bannir l'IP au niveau du pare-feu, ce qui épargne aussi les autres services de la machine.

```text
# /etc/fail2ban/filter.d/nginx-robots-usurpes.conf
[Definition]
failregex = ^<HOST> \[
ignoreregex =
```

```text
# /etc/fail2ban/jail.d/nginx-robots-usurpes.local
[nginx-robots-usurpes]
enabled  = true
filter   = nginx-robots-usurpes
logpath  = /var/log/nginx/robots-usurpes.log
port     = http,https
maxretry = 3
findtime = 10m
bantime  = 1d
```

```bash
fail2ban-regex /var/log/nginx/robots-usurpes.log /etc/fail2ban/filter.d/nginx-robots-usurpes.conf
systemctl reload fail2ban
fail2ban-client status nginx-robots-usurpes
```

Derrière un CDN, un bannissement au pare-feu de la machine est inopérant, puisque les connexions arrivent depuis le CDN : appliquez alors la règle au niveau du CDN.

## Choisir la réponse adaptée

Un imitateur n'est pas forcément malveillant : il peut s'agir d'un outil de test, d'un service de veille ou d'un développeur qui vérifie son propre robots.txt. Graduez la réponse selon le volume et le comportement observés.

| Situation | Réponse proportionnée |
|---|---|
| Quelques requêtes isolées | Les consigner, sans bloquer |
| Exploration soutenue avec un faux User-Agent | Code 403 via nginx, comme ci-dessus |
| Exploration agressive, plusieurs adresses | Bannissement fail2ban, ou règle au niveau du CDN |
| Charge excessive d'un vrai robot | robots.txt (et `Crawl-delay` pour les robots qui le respectent, comme ClaudeBot et CCBot), puis contact de l'éditeur |

Pour un vrai robot trop insistant, le blocage par IP n'est pas la bonne voie : Anthropic, Amazon et Meta publient une adresse de contact dans leur documentation, et Anthropic demande d'écrire depuis une adresse du domaine concerné pour que la demande soit vérifiable.

Surveillez enfin le journal des imitateurs après chaque mise à jour des listes : une hausse soudaine de « vrais » robots classés comme imitateurs signale presque toujours une liste périmée ou un problème de CDN, pas une attaque.

## Ce qu'il ne faut pas faire

- Bloquer les plages IP 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 ; utilisez robots.txt pour exprimer un refus.
- Se fier au DNS inverse pour OpenAI, Anthropic ou Perplexity : aucune de ces documentations ne le prévoit.
- Recopier les plages une fois pour toutes : elles évoluent, et une liste périmée finira par bloquer un vrai robot.
- Déployer sans tester : une expression régulière trop large dans `map` peut classer du trafic légitime comme imitateur. Contrôlez la configuration avec `nginx -t`, le filtre avec `fail2ban-regex`, et relisez les premières lignes de `robots-usurpes.log`.

Pour vérifier votre site, utilisez le [scanner IABIG](/fr/).

## Sources officielles

- 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, Verifying Googlebot and other Google crawlers : <https://developers.google.com/crawling/docs/crawlers-fetchers/verify-google-requests>
- Apple, About Applebot : <https://support.apple.com/en-us/119829>
- Common Crawl, CCBot et FAQ : <https://commoncrawl.org/ccbot> et <https://commoncrawl.org/faq>
- nginx, modules geo, map, realip et log : <https://nginx.org/en/docs/http/ngx_http_geo_module.html>, <https://nginx.org/en/docs/http/ngx_http_map_module.html>, <https://nginx.org/en/docs/http/ngx_http_realip_module.html>, <https://nginx.org/en/docs/http/ngx_http_log_module.html>
- fail2ban : <https://github.com/fail2ban/fail2ban>
