Postmortem — CrowdSec AppSec : faux positif heuristique sur Sonarr/Radarr

Résumé
Le 25 mai 2026 vers 22h02 (heure locale), Sonarr et Radarr sont devenus totalement inaccessibles depuis l’IP maison (82.XX.XX.XX), retournant 403 sur toutes les URLs y compris /login. Le service était pourtant opérationnel. Le diagnostic initial suspectait les récents déploiements du refactor crowdsec-cf-sync — la cause réelle est un faux positif heuristique CrowdSec AppSec.
Timeline
| Heure (locale) | Événement |
|---|---|
| ~22h00 | Cookie de session Sonarr expiré côté navigateur |
| 22h02:31 | Le navigateur charge la bibliothèque Sonarr → tente de charger 20+ /MediaCover/*.jpg simultanément |
| 22h02:31 | Sonarr retourne 302 → /login pour chaque image (session invalide) |
| 22h02:34 | La connexion SignalR WebSocket s’établit (101) via access_token dans l’URL |
| ~22h05 | CrowdSec AppSec déclenche la règle heuristique http-probing : rafale de requêtes échouées depuis la même IP |
| 22h11:37 | Toutes les requêtes de 82.XX.XX.XX retournent 403 — cs_reason=heuristic dans les logs nginx |
| 22h13:34 | Même /login bloqué — l’IP ne peut plus s’authentifier |
Cause racine
CrowdSec AppSec maintient un état heuristique en mémoire, distinct des décisions LAPI. Lorsque le navigateur tente de charger simultanément de nombreuses ressources et reçoit des 302/403 de l’application upstream (Sonarr), AppSec interprète cette rafale d’échecs comme du sondage agressif (http-probing) et bloque l’IP source.
Le problème : l’IP maison est dans l’allowlist LAPI (my_allowlist) et dans le bloc geo $lan nginx, mais l’heuristique AppSec ne consulte pas l’allowlist LAPI — elle opère indépendamment sur des signaux comportementaux.
cscli decisions list --ip 82.XX.XX.XX
→ No active decisions ← l'allowlist LAPI est respectée
nginx access log :
82.XX.XX.XX ... "GET /" 403 ... cs_reason=heuristic ← AppSec en mémoire bloqueCe qui n’était PAS la cause
Plusieurs pistes ont été écartées dans l’ordre :
| Piste | Résultat |
|---|---|
Refactor crowdsec-cf-sync (redéploiement du jour) |
❌ Ne touche pas nginx, AppSec, ni Sonarr |
Ban LAPI CrowdSec (cscli decisions list) |
❌ Aucune décision active pour l’IP |
bans.json Lua IPC |
❌ 3 IPs tierces uniquement, pas l’IP maison |
Bloc nginx geo $lan |
❌ IP présente dans le bloc ($lan = 1) |
| API key Sonarr invalide | ❌ /api/v3/system/status retourne 200 avec la clé |
| Problème réseau / VM Sonarr | ❌ VM répond (302 attendu pour session expirée) |
Impact
- Durée : ~15–20 minutes (détection manuelle)
- Périmètre : Sonarr et Radarr sur
vm-mediauniquement - Utilisateurs : 1 (IP maison)
- Données : aucune perte
Fix immédiat
Redémarrage de CrowdSec pour vider l’état heuristique en mémoire :
sudo systemctl restart crowdsecFix permanent
Ajout d’un bypass Lua dans /etc/openresty/snippets/crowdsec_access.conf pour exempter les IPs LAN de tout check AppSec avant même la vérification LAPI :
access_by_lua_block {
if ngx.var.lan == "1" then return end
require("crowdsec.access").check()
}La variable $lan est définie par le bloc geo dans conf.d/lan.conf et couvre les plages RFC 1918 (192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12) ainsi que l’IP maison fixe.
Les IPs externes continuent à passer par le check complet (LAPI + AppSec).
Leçons
-
L’allowlist LAPI ne protège pas contre l’heuristique AppSec. Les deux systèmes sont indépendants — une IP peut être dans
my_allowlistet quand même être bloquée en mémoire par AppSec. -
Les rafales légitimes déclenchent les règles heuristiques. Sonarr et Radarr chargent des dizaines d’images couverture au premier affichage — pattern indiscernable d’un scan d’assets.
-
cs_nginx_ban: falsene signifie pas “non bloqué”. Ce champ reflète uniquement le bouncer LAPI. Un bloc AppSec heuristique retourne 403 aveccs_reason=heuristicdans le log nginx — champ absent du pipeline Vector/BetterStack. -
Le diagnostic via BetterStack a ses limites. La source
nginx_statusn’inclut pascs_reason— la cause réelle n’était visible que dans les logs nginx bruts (/var/log/nginx/vm-media.access.log).