Contenu

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 bloque

Ce 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-media uniquement
  • 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 crowdsec

Fix 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

  1. L’allowlist LAPI ne protège pas contre l’heuristique AppSec. Les deux systèmes sont indépendants — une IP peut être dans my_allowlist et quand même être bloquée en mémoire par AppSec.

  2. 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.

  3. cs_nginx_ban: false ne signifie pas “non bloqué”. Ce champ reflète uniquement le bouncer LAPI. Un bloc AppSec heuristique retourne 403 avec cs_reason=heuristic dans le log nginx — champ absent du pipeline Vector/BetterStack.

  4. Le diagnostic via BetterStack a ses limites. La source nginx_status n’inclut pas cs_reason — la cause réelle n’était visible que dans les logs nginx bruts (/var/log/nginx/vm-media.access.log).