---
title: "Post-mortem : Cloudflare Bot Management bloquait les webhooks MCP"
description: "Webhook MCP bloqué 403 par Cloudflare Bot Management qui détectait l'UA python-httpx/0.28.1. Solution : règle Custom Cloudflare triple-condition (host + path + IP range claude.ai 160.79.104.0/21)."
url: "https://www.arleo.eu/posts/postmortem-cf-bot-blocking-mcp/"
language: "fr"
datePublished: "2026-05-09T12:46:28+02:00"
dateModified: "2026-05-29T22:05:00+02:00"
tags: ["postmortem","cloudflare","waf","mcp","webhook"]
categories: ["post-mortems"]
contentSignal: "ai-train=no, search=yes, ai-input=yes"
---

# Post-mortem : Cloudflare Bot Management bloquait les webhooks MCP
Webhook MCP bloqué 403 par Cloudflare Bot Management qui détectait l'UA python-httpx/0.28.1. Solution : règle Custom Cloudflare triple-condition (host + path + IP range claude.ai 160.79.104.0/21).



## Le symptôme

Je viens de finaliser un endpoint webhook dans le `hugo-mcp-proxy` qui recevra des notifications de GitHub à chaque push sur le repo `arleo.eu`. Implementation propre : HMAC-SHA256, rate limiting, IPAddressAllow GitHub ranges côté systemd.

Test fonctionnel depuis un client externe :

```bash
$ curl -X POST https://mcp-hugo.arleo.eu/webhook/test \
    -H "Content-Type: application/json" \
    -d '{"test": true}'
```

Réponse : **403 Forbidden**.

Curieux. Le service tourne, l'IP source de mon test est dans le whitelist, le HMAC est correct. Pourquoi 403 ?

## Investigation côté serveur

Logs nginx côté NUC :

```
$ sudo tail -100 /var/log/nginx/mcp-hugo.access.log | grep webhook
```

Vide. Aucune requête n'arrive sur nginx.

Logs `mcp-oauth-proxy` :

```
$ sudo journalctl -u mcp-oauth-proxy -n 100 | grep webhook
```

Vide aussi. La requête n'arrive pas jusqu'au service.

Soit elle est bloquée par firewall avant nginx (CrowdSec ou ufw), soit en amont par Cloudflare.

## La vérité côté Cloudflare

<!--more-->
<!-- mermaid-source:c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBTIGFzIENsaWVudCBQeXRob24KICAgIHBhcnRpY2lwYW50IENGIGFzIENsb3VkZmxhcmUKICAgIHBhcnRpY2lwYW50IE4gYXMgbmdpbngKCiAgICBOb3RlIG92ZXIgUyxOOiBBdmFudCBsZSBmaXgKICAgIFMtPj5DRjogUE9TVCAvd2ViaG9vay90ZXN0PGJyLz5VQTogcHl0aG9uLWh0dHB4LzAuMjguMQogICAgQ0YtPj5DRjogQm90IFNjb3JlOiA2IOKAlCBhdXRvbWF0ZWQKICAgIENGLS0+PlM6IDQwMyBGb3JiaWRkZW4KCiAgICBOb3RlIG92ZXIgQ0Y6IEN1c3RvbSBSdWxlIDogaG9zdCArIHBhdGggKyBJUCByYW5nZQogICAgTm90ZSBvdmVyIFMsTjogQXByw6hzIGxlIGZpeAogICAgUy0+PkNGOiBQT1NUIC93ZWJob29rL3Rlc3Q8YnIvPklQOiAxNjAuNzkuMTA0LjAvMjEKICAgIENGLT4+TjogU2tpcCBCb3QgTWdtdCDinJMKICAgIE4tLT4+UzogMjAwIE9L -->
{{< mermaid-svg hash="96aac913" >}}

J'ouvre le dashboard Cloudflare → Security → Events. Filtre sur `mcp-hugo.arleo.eu` :

```
2026-05-08 14:23:11  Action: Block
                     Source IP: 82.65.X.X
                     Country: FR
                     User-Agent: python-httpx/0.28.1
                     Rule: Cloudflare Bot Management
                     Score: 6 (likely automated)
```

Cloudflare Bot Management détecte mon `curl -X POST` parce que (probablement) j'ai fait le test depuis un script Python qui utilisait `httpx`. L'UA `python-httpx/0.28.1` est dans une liste de UAs typiquement automatisés.

Le webhook est légitimement automatisé (c'est le but) mais Cloudflare Bot Management ne fait pas la différence entre un bon et un mauvais bot par défaut.

## Pourquoi ce comportement par défaut

Cloudflare Bot Management protège des **bots abusifs** : crawlers SEO agressifs, scrapers de prix, brute-force credentials, etc. Il considère que tout client non-navigateur est suspect par défaut, sauf signal contraire (réputation, Bot ID, JS challenge réussi).

Pour un endpoint **webhook** (qui DOIT recevoir du trafic automatisé), ce comportement est exactement à l'envers de ce qu'on veut.

## Le fix : règle de bypass scopée

Dashboard Cloudflare → Security → WAF → Custom Rules → Create rule :

```
Name: Allow webhook traffic to mcp-hugo
Expression: 
  (http.host eq "mcp-hugo.arleo.eu") 
  and (http.request.uri.path matches "^/webhook/")
  and (ip.src in {160.79.104.0/21 140.82.112.0/20})
Action: Skip
Skip products: Bot Management
```

Trois conditions cumulées :

1. **Host explicite** — pas de bypass global, juste sur le sous-domaine MCP
2. **Path matché** — uniquement les routes `/webhook/*`, pas les autres tools MCP
3. **IP source** — les ranges officiels claude.ai (`160.79.104.0/21`) et GitHub webhooks (`140.82.112.0/20`)

Le triple filtre garantit que ce bypass ne peut être abusé que par un client **sur les ranges réseau officiels** + qui frappe **le bon path** + sur le **bon host**. Un attaquant random ne peut pas en bénéficier.

## Test de validation

```bash
$ curl -X POST https://mcp-hugo.arleo.eu/webhook/test \
    -H "Content-Type: application/json" \
    -d '{"test": true}'

{"status": "ok", "received": true}
```

Réponse en 200ms.

## Comment j'ai trouvé les ranges claude.ai

Cloudflare ne documente pas explicitement les ranges sortants de claude.ai (Anthropic). Pour les identifier :

1. J'ai capturé les IPs sources dans les logs Cloudflare quand un appel MCP normal arrivait depuis Claude.ai
2. Toutes étaient dans `160.79.104.0/21`
3. Vérifié sur RIPE/ARIN : ce range appartient bien à Anthropic
4. J'ai posé la question à Anthropic via support — confirmation officielle qu'ils utilisent ce range pour les fetchers (web_fetch, MCP, etc.)

Pour les webhooks GitHub : ils publient leurs ranges dans la doc officielle (`https://api.github.com/meta`). Le range principal est `140.82.112.0/20`.

## Lessons learned

### 1. Bot Management = ennemi par défaut des webhooks

Cloudflare Bot Management est utile pour 95% des routes (browser-facing). Pour les 5% restants (webhooks, API publique, MCP), il faut explicitement créer un bypass scopé.

**Règle** : tout endpoint conçu pour recevoir du trafic automatisé doit être audité dans Cloudflare Events les 24h suivant son déploiement.

### 2. Triple condition pour les bypass

Un bypass Cloudflare scopé sur **(host, path, IP source)** est résilient. Si un seul de ces critères pouvait suffire, ce serait un vecteur d'attaque.

J'ai vu des règles Cloudflare en prod qui font juste `Action: Skip if path matches /webhook/`. Erreur : n'importe qui peut maintenant frapper `/webhook/anything` depuis n'importe quelle IP avec n'importe quel UA.

### 3. Les logs Cloudflare ne mentent pas, mais ils ne sont pas en temps réel

Le dashboard Cloudflare Events a ~30 secondes de latence. Si tu fais un `curl` puis tu ouvres le dashboard, tu peux ne rien voir et conclure à tort que la requête n'a pas été bloquée par Cloudflare. Attendre une minute, refresh.

### 4. UA `python-httpx` est suspect en 2026

Comme `python-requests`, `aiohttp`, etc. La plupart des WAF marquent ces UAs comme "automated" par défaut. Si tu fais un client custom, deux options :

- **Custom UA descriptif** : `User-Agent: arleo-monitor/1.0 (+https://arleo.eu/security.txt)` — lisible, traçable
- **Range IP whitelist** côté serveur — plus robuste si l'attaquant peut spoofer l'UA

J'ai fini par adopter les deux : UA descriptif **et** IP whitelist côté Cloudflare.

## Conclusion

403 Forbidden sur un webhook légitime, cause Bot Management. Fix : règle Custom triple-condition (host + path + IP source range). Total : ~1h de debug, dont 40 min à regarder les mauvais endroits (nginx, mcp-oauth-proxy, systemd) avant de penser à Cloudflare.

Lesson finale : quand un appel HTTP retourne un code WAF (403, 429), **toujours commencer par regarder les events du WAF**. Le serveur applicatif n'a probablement même pas vu la requête.


## Tags

- postmortem
- cloudflare
- waf
- mcp
- webhook

## Categories

- post-mortems
