Post-Mortem — OpenResty 500 : module Lua CrowdSec

- Date : 16–17 août 2026
- Sévérité : P1
- Statut : Résolu
- Impact : HTTP 500 sur l’origine, y compris sur du trafic légitime et le endpoint de monitoring
/ping
En bref
Deux rafales de scan Internet ont révélé un défaut latent dans la chaîne OpenResty/CrowdSec. Les scanners n’ont pas « fait tomber » OpenResty par leur volume : la cadence restait faible pour nginx. Ils ont surtout déclenché un chemin d’exécution qui appelait un module Lua custom impossible à retrouver sur des workers OpenResty fraîchement chargés.
La cause racine était ce chargement dans access_by_lua :
require("crowdsec.access")alors que le lua_package_path actif ne contenait pas le répertoire où vivait le module custom crowdsec/access.lua.
Le résultat : un worker qui devait charger ce module à froid renvoyait HTTP 500 avant même d’arriver à l’authentification ou au contenu Hugo.
Ce qui a déclenché l’incident
Deux sources ont été observées pendant l’enquête :
34.75.198.72— première vague vers 08:27 UTC, puis nouvelle série de probes vers 08:35 ;34.169.50.247— nouvelle rafale vers 09:14 UTC, avec environ 95 requêtes en 12 secondes dont 94 en HTTP 500.
Les scans visaient notamment des chemins de configuration, de secrets et d’outils IA/MCP :
/.env
/.claude.json
/.claude/settings.json
/.mcp.json
/.codex/config.toml
/.cursor/mcp.json
/config/secrets.yml
/config/master.key
/application.yml
/appsettings.json
/google-credentials.json
/service-account.json
/debug/pprof/
/metrics
/@fs/proc/self/environ?import&raw??Une partie du trafic utilisait le User-Agent :
Mozilla/5.0 (compatible; GrokBot/1.0; +https://x.ai/grokbot)Ce User-Agent est trivialement falsifiable : cela ne constitue pas une attribution à xAI.
Timeline condensée
| Heure UTC | Événement |
|---|---|
| ~08:27 | Première rafale depuis 34.75.198.72 : configs IA/MCP, secrets et fichiers sensibles |
| ~08:28 | Des requêtes malveillantes et légitimes commencent à recevoir HTTP 500 |
| ~08:35 | Deuxième vague de probes PHP/webshell depuis la même source |
| 09:08 | CrowdSec bannit finalement 34.75.198.72 via crowdsecurity/http-crawl-non_statics |
| 09:14:30–09:14:42 | 34.169.50.247 envoie ~95 requêtes ; 94 retournent 500 |
| 09:16 | Les sondes BetterStack sur /ping reçoivent elles aussi 500 depuis plusieurs régions |
| 17 août, investigation | Les logs OpenResty révèlent module 'crowdsec.access' not found |
| 13:20 locale | Premier correctif du lua_package_path, reload et disparition des 500 |
| 13:23 locale | Correctif durable remplacé par un symlink hors du fichier réécrit par le paquet |
| 11:31 UTC | BetterStack confirme /ping de nouveau UP |
Signature exacte de la panne
Dans /var/log/nginx/www.arleo.eu.error.log :
lua entry thread aborted: runtime error:
access_by_lua(snippets/crowdsec_access.conf:26):3:
module 'crowdsec.access' not found:
no field package.preload['crowdsec.access']
no file '/usr/local/openresty/nginx//../lualib/plugins/crowdsec/crowdsec/access.lua'
...Le bloc concerné était simple :
access_by_lua_block {
if ngx.var.lan == "1" then return end
require("crowdsec.access")
}Le require() n’était pas encapsulé et son échec faisait donc échouer la requête entière.
Le lua_package_path chargé par le bouncer officiel était :
lua_package_path '$prefix/../lualib/plugins/crowdsec/?.lua;;';alors que les modules custom vivaient sous :
/usr/local/openresty/nginx/conf/lua/crowdsec/access.lua, mitigation.lua, sync.lua, heuristics.lua, tarpit.lua et waf_refs.lua existaient bien : c’est leur chemin de recherche qui manquait.
Pourquoi le bug semblait intermittent
require() met en cache un module uniquement lorsqu’il a été chargé avec succès. Avec lua_code_cache on, un worker qui avait déjà chargé crowdsec.access pouvait continuer à fonctionner normalement.
En revanche, tout worker neuf ou recyclé qui devait refaire le require() utilisait le mauvais lua_package_path et tombait en 500.
Cela explique pourquoi :
- des pages pouvaient sembler normales derrière le cache Cloudflare ;
/ping, enno-store, continuait à atteindre l’origine et exposait le bug ;- une rafale de scan pouvait faire apparaître brutalement plusieurs workers défectueux ;
- le problème ressemblait à une surcharge alors que le volume lui-même n’était pas anormal pour OpenResty.
Faux diagnostic initial
Au début, plusieurs pistes semblaient crédibles :
- contenu Hugo récemment modifié ;
- vhost apex
arleo.eucassé alors quewww.arleo.eufonctionnait ; - surcharge OpenResty/CrowdSec sous scan ;
- problème d’authentification du endpoint
/ping.
Le MCP Hugo a permis d’éliminer immédiatement le contenu : build sain, publication cohérente, pages publiques présentes.
Le point décisif a été de lire le log OpenResty local complet, pas seulement les métriques HTTP agrégées : le même traceback Lua apparaissait pour chaque 500.
Correctif appliqué
Premier correctif : ajouter le chemin manquant
Le correctif immédiat a été :
lua_package_path '$prefix/../lualib/plugins/crowdsec/?.lua;$prefix/lua/?.lua;;';Puis :
sudo openresty -t
sudo openresty -s reloadAprès reload : nouveaux workers, plus de module 'crowdsec.access' not found, plus de 500.
Correctif durable : ne pas modifier le fichier réécrit par le paquet
Le fichier crowdsec_openresty.conf est réécrit par les mises à jour du bouncer CrowdSec et n’est pas protégé comme un conffile dpkg. Modifier directement son lua_package_path ferait donc revenir le bug à une future mise à jour.
Le fichier a été remis dans son état stock et un lien symbolique a été créé :
/usr/local/openresty/lualib/plugins/crowdsec/crowdsec
-> /usr/local/openresty/nginx/conf/lua/crowdsecAinsi le chemin officiel :
.../plugins/crowdsec/?.luarésout naturellement :
require("crowdsec.access")
→ .../plugins/crowdsec/crowdsec/access.luasans modifier le fichier géré par le paquet.
Validation après correction
Les contrôles effectués après reload :
openresty -t: OK ;- nouveaux workers OpenResty démarrés proprement ;
- 15 requêtes séquentielles sur
/ping: aucune 500 ; - 30 requêtes concurrentes sur
/ping: aucune 500 ; - aucune nouvelle occurrence de
module 'crowdsec.access' not found; - aucune 500 dans l’access log après le correctif ;
/et les pages Hugo : 200 ;- apex
arleo.eu: redirection 301 verswww; - BetterStack : monitor
/pingrepassé UP après le correctif.
Le 401 observé sans credentials sur /ping est normal : ce endpoint est protégé par Basic Auth. BetterStack possédait déjà la bonne configuration d’authentification ; auparavant le crash access_by_lua se produisait avant auth_basic.
Si cela se reproduit
Ne pas commencer par rollbacker Hugo.
1. Vérifier immédiatement la signature Lua
sudo grep -n "crowdsec.access\|lua entry thread aborted" \
/var/log/nginx/www.arleo.eu.error.log | tail -50Si on voit :
module 'crowdsec.access' not foundla cause est probablement la même.
2. Vérifier que le module est résolvable
ls -la /usr/local/openresty/lualib/plugins/crowdsec/crowdsec
ls -la /usr/local/openresty/nginx/conf/lua/crowdsec/access.luaLe premier chemin doit être un symlink vers le dossier custom.
3. Vérifier la configuration réellement chargée
sudo openresty -T 2>&1 | grep -n "lua_package_path\|crowdsec_access"4. Tester avant reload
sudo openresty -tAttention : openresty -t vérifie la syntaxe mais n’exécute pas le require() de la phase request. Un test HTTP réel est donc obligatoire.
5. Tester le chemin d’exécution réel
curl -sk -o /dev/null -w '%{http_code}\n' https://www.arleo.eu/pingSans auth, un 401 propre est préférable à un 500. Avec les credentials du monitor, le résultat attendu est 200.
6. Vérifier l’absence de nouvelles 500
sudo tail -f /var/log/nginx/www.arleo.eu.error.logpuis lancer plusieurs requêtes concurrentes.
Conclusion
Les scanners 34.75.198.72 et 34.169.50.247 ont été les déclencheurs observés, mais pas la cause racine. Une centaine de requêtes en quelques secondes ne devrait pas mettre OpenResty à terre.
La véritable défaillance était un module Lua custom CrowdSec présent sur disque mais absent du chemin de recherche des workers OpenResty. La sécurité était elle-même devenue un point de panne global : une erreur de require() dans access_by_lua transformait n’importe quelle requête en HTTP 500 avant le reste de la chaîne nginx.
La leçon principale est simple : quand une panne semble corrélée à une attaque, vérifier que l’attaque ne fait pas seulement apparaître un défaut latent de notre propre couche de défense.
