Post-Mortem Cloudflare : Quand un Fichier de Configuration Double de Taille
Analyse technique de la panne Cloudflare du 18 novembre 2025 causée par un changement de permissions ClickHouse - Leçons sur la résilience des systèmes distribués
Post-Mortem Cloudflare : Quand un Fichier de Configuration Double de Taille
Le 18 novembre 2025, Cloudflare a connu sa pire panne depuis 2019, affectant les services CDN, WAF, Workers et Access pendant près de 6 heures. Contrairement aux apparences initiales, ce n'était ni une cyberattaque ni un problème réseau BGP, mais un changement apparemment anodin dans les permissions d'une base de données ClickHouse.
🔴 Chronologie de l'Incident
| Heure (UTC) | Événement |
|---|---|
| 11:05 | Déploiement du changement de permissions ClickHouse |
| 11:28 | Début de la panne - Premières erreurs 5xx |
| 11:35 | Création de l'incident call |
| 13:05 | Mise en place de bypasses pour Workers KV et Access |
| 14:30 | Résolution principale - Déploiement du bon fichier |
| 17:06 | Tous les services restaurés |
Durée totale : ~6 heures
Impact
- Services principaux down : CDN, WAF, Bot Management
- Services partiellement affectés : Workers KV, Access, Dashboard
- Taux d'erreur : Pic massif de codes HTTP 5xx
- Utilisateurs impactés : Millions à travers le monde
🔍 Cause Racine : Un Changement de Permissions ClickHouse
Le Système Affecté
Cloudflare utilise ClickHouse pour générer un fichier de configuration pour son système Bot Management. Ce fichier contient des "features" (caractéristiques) utilisées par un modèle de machine learning pour détecter les bots.
Fonctionnement normal :
ClickHouse Query
↓
Génère fichier de features (~60 features)
↓
Déploie toutes les 5 minutes sur le réseau global
↓
Bot Management utilise les features pour scorer le trafic
Architecture ClickHouse
ClickHouse Cluster:
- Database: default (tables distribuées)
- Database: r0 (tables sous-jacentes par shard)
Requêtes distribuées:
- Via compte système partagé
- Accès implicite à r0 (invisible aux utilisateurs)
Le Changement Fatal
À 11:05 UTC, Cloudflare a déployé un changement pour améliorer la sécurité :
Objectif : Rendre l'accès aux tables r0 explicite (au lieu d'implicite)
Bénéfice attendu :
- Meilleure gestion des permissions
- Limites par requête plus granulaires
- Meilleure visibilité pour les utilisateurs
Problème imprévu : Une requête critique n'était pas filtrée par database :
-- Requête utilisée pour générer le fichier de features
SELECT name, type
FROM system.columns
WHERE table = 'http_requests_features'
ORDER BY name;
Avant le changement :
Retourne uniquement les colonnes de `default.http_requests_features`
→ ~60 features
Après le changement :
Retourne les colonnes de:
- default.http_requests_features
- r0.http_requests_features (sur chaque shard)
→ Plus de 200 features (duplicatas)
💥 La Cascade de Pannes
1. Limite de Taille Dépassée
Le proxy Cloudflare (FL2) a une limite codée en dur :
// Code Rust dans FL2
const MAX_FEATURES: usize = 200;
fn load_bot_features(file: &FeatureFile) -> Result<()> {
if file.features.len() > MAX_FEATURES {
return Err("Too many features"); // 🔥 PANIC ICI
}
// ...
}
Pourquoi cette limite ?
- Pré-allocation mémoire pour performance
- Éviter consommation mémoire non bornée
- Limite "safe" : 200 >> 60 features normaux
2. Panic Non Géré
thread fl2_worker_thread panicked:
called Result::unwrap() on an Err value
Résultat : Le proxy crashe pour chaque requête utilisant Bot Management
3. Propagation Intermittente
Le cluster ClickHouse était mis à jour progressivement :
Toutes les 5 minutes :
- Si requête hit un node MIS À JOUR → Mauvais fichier (>200 features)
- Si requête hit un node NON MIS À JOUR → Bon fichier (~60 features)
Résultat : Système oscille entre "marche" et "ne marche pas"
Cette intermittence a induit l'équipe en erreur : ils ont cru à une attaque DDoS !
🎭 Les Fausses Pistes
Piste 1 : Attaque DDoS
Symptômes trompeurs :
- Fluctuations rapides de disponibilité
- Le status page de Cloudflare (hébergé ailleurs) était aussi down (coïncidence)
- Ressemble aux récentes attaques Aisuru (15 Tbps)
Message interne de l'équipe :
11:35 - Incident Chat Room
"Could this be a continuation of recent Aisuru DDoS attacks?"
"Status page is down too - coordinated attack?"
Piste 2 : Problème Workers KV
Symptôme initial : Workers KV montrait des erreurs élevées
Action prise : Mitigation sur Workers KV, limitation de comptes
Réalité : Workers KV était une victime collatérale (dépend du core proxy)
🛠️ La Résolution
Étape 1 : Isolation (13:05)
# Bypass du core proxy pour Workers KV et Access
# → Utiliser l'ancienne version du proxy (FL)
# Impact de FL vs FL2 :
# - FL2 : Panic → 5xx errors
# - FL : Pas de panic, mais bot score = 0 pour tout le trafic
Étape 2 : Identification (14:24)
# L'équipe identifie la cause :
1. Le fichier Bot Management est trop gros
2. Stopper la génération automatique
3. Tester un ancien fichier connu bon
Étape 3 : Déploiement du Fix (14:30)
# 1. Stopper propagation du mauvais fichier
stop_bot_feature_generation()
# 2. Injecter manuellement un bon fichier
inject_known_good_file(version="11:00-UTC")
# 3. Forcer restart du core proxy
force_restart_core_proxy()
Étape 4 : Restauration Complète (17:06)
- Redémarrage de tous les services downstream
- Retour à la normale du trafic
- Résolution du backlog de requêtes
💡 Leçons Techniques
1. Toujours Filtrer par Database
Code problématique :
SELECT name, type
FROM system.columns
WHERE table = 'http_requests_features' -- ❌ Pas de filtre database
ORDER BY name;
Code correct :
SELECT name, type
FROM system.columns
WHERE
database = 'default' AND -- ✅ Filtre explicite
table = 'http_requests_features'
ORDER BY name;
2. Gérer les Erreurs Gracefully
Code fragile (Rust) :
let features = load_features(&file).unwrap(); // ❌ Panic si erreur
Code robuste :
let features = match load_features(&file) {
Ok(f) => f,
Err(e) => {
log_error!("Failed to load features: {}", e);
metrics::increment("bot_feature_load_error");
// Fallback sur cache ou ancienne version
return load_cached_features();
}
};
3. Valider les Inputs, Même Internes
Principe :
// Ne JAMAIS assumer qu'un fichier généré en interne est valide
fn validate_feature_file(file: &FeatureFile) -> Result<()> {
// Vérifications
if file.features.len() > MAX_FEATURES {
return Err("Too many features");
}
if file.features.len() == 0 {
return Err("Empty feature file");
}
// Vérifier duplicates
if has_duplicates(&file.features) {
return Err("Duplicate features detected");
}
Ok(())
}
// Utiliser AVANT de propager
validate_feature_file(&file)?;
propagate_to_network(&file);
4. Canary Deployments
Erreur de Cloudflare :
Changement déployé graduellement sur le cluster ClickHouse
MAIS fichier propagé instantanément sur TOUT le réseau
Meilleure approche :
def deploy_feature_file(file):
# 1. Valider
if not validate(file):
raise ValueError("Invalid file")
# 2. Deploy canary (1% du trafic)
deploy_to_canary_nodes(file, percentage=0.01)
# 3. Monitor 5 minutes
if error_rate > threshold:
rollback()
return
# 4. Deploy progressivement
for percentage in [10, 25, 50, 100]:
deploy_to_nodes(file, percentage)
monitor(duration=300) # 5 min
5. Kill Switches Globaux
Ce que Cloudflare a annoncé :
# Global kill switches pour chaque feature
features:
bot_management:
kill_switch: true # Désactive instantanément sur tout le réseau
fallback: "allow_all" # Comportement par défaut si désactivé
📊 Impact par Service
| Service | Impact | Cause |
|---|---|---|
| Core CDN | 5xx errors | Core proxy panic |
| Bot Management | Score = 0 (FL) ou erreur (FL2) | Feature file invalid |
| Workers KV | 5xx errors élevés | Dépend du core proxy |
| Cloudflare Access | Auth failures | Dépend du core proxy |
| Dashboard | Indisponible (login) | Turnstile down + KV down |
| Turnstile | Failed to load | Core proxy down |
| Email Security | Perte IP reputation | Dépendance indirecte |
🎯 Actions Correctives de Cloudflare
Court Terme
- ✅ Hardening de l'ingestion des fichiers de config (traiter comme input utilisateur)
- ✅ Kill switches globaux pour toutes les features
- ✅ Limiter les core dumps pour éviter saturation CPU
- ✅ Revoir tous les failure modes dans les modules du proxy
Moyen Terme
- Tests de chaos engineering sur les configurations
- Validation automatique des fichiers avant propagation
- Monitoring de la taille des fichiers de config
- Alertes sur anomalies de métadonnées ClickHouse
Long Terme
- Architecture plus résiliente avec fallbacks
- Isolation plus forte entre les modules
- Découplage des dépendances critiques
💭 Réflexions
La Fragilité des Systèmes Complexes
Changement de permission "sécurité"
→ Requête non filtrée retourne duplicatas
→ Fichier double de taille
→ Dépasse limite codée en dur
→ Panic non géré
→ Core proxy crash
→ 17% du web down
Un seul maillon faible dans une chaîne de 6 étapes.
L'Importance des Limites
La limite de 200 features était là pour protéger (pré-allocation mémoire).
Mais elle est devenue un point de défaillance car :
- Pas de validation en amont
- Erreur non gérée gracefully
- Pas de fallback
Leçon : Les limites de sécurité doivent avoir des mécanismes de dégradation gracieuse.
La Difficulté du Diagnostic
L'intermittence (bon fichier / mauvais fichier toutes les 5 min) a fait perdre 3 heures à l'équipe.
Leçon : Les failures intermittents sont les plus durs à débugger.
🔗 Ressources
- Post-Mortem Officiel Cloudflare
- Architecture FL2 Proxy
- ClickHouse Documentation
- Rust Error Handling Best Practices
💬 Conclusion
Cette panne rappelle que dans les systèmes distribués à grande échelle :
- Aucun changement n'est anodin - Un changement de permissions a causé 6h de panne
- Toujours filtrer explicitement - Ne jamais assumer qu'une requête retournera ce qu'on attend
- Gérer les erreurs gracefully - Un
unwrap()peut faire tomber 17% du web - Valider même l'input interne - Traiter les configs internes comme des inputs externes
- Avoir des kill switches - Pour désactiver rapidement une feature problématique
Question pour vous : Avez-vous des limites codées en dur dans votre code qui pourraient devenir des points de défaillance ? 🤔