Contenu

Audit sécurité NUC : ModSecurity supprimé, 500 MB récupérés

⚡ En bref

Un audit de la stack sécurité du NUC révèle une double inspection WAF redondante : ModSecurity + OWASP CRS chargent 11 872 règles en mémoire malgré SecRuleEngine Off, en parallèle d’un CrowdSec AppSec déjà actif et couvrant le même périmètre. Après suppression du module nginx ModSecurity et 5 autres correctifs ciblés, nginx passe de ~520 MB à ~27 MB PSS. Sécurité identique, empreinte divisée par 20.


🏗️ Architecture avant audit

La stack sécurité reposait sur six couches empilées :

Internet
    ↓
Cloudflare WAF  (edge WAF + DDoS + CrowdSec auto-ban)
    ↓
nginx  (Lua bouncer CrowdSec)
    ↓
ModSecurity + OWASP CRS   ←── double inspection
CrowdSec AppSec            ←── double inspection
    ↓
CrowdSec agent v1.7.8  (scénarios : http-cve, nginx, linux, sshd)
    ↓
Bouncers : firewall-bouncer (nftables) + nginx-bouncer (Lua) + cf-sync
    ↓
Backend (Hugo VM 192.168.122.69 / PHP-FPM)

/images/audit_schema.png

Le problème identifié : ModSecurity et CrowdSec AppSec inspectent chaque requête de façon indépendante. Le CRS charge ses regex en mémoire même quand SecRuleEngine Off est positionné — le module est chargé, les règles compilées, rien n’est inspecté mais la RAM est consommée.

Empreinte mémoire initiale (smem PSS)

ComposantPSS
nginx (master + 4 workers)~520 MB
CrowdSec agent~170 MB
firewall-bouncer~30 MB
Total stack sécurité~720 MB

🔍 6 problèmes identifiés

1. Double inspection ModSecurity / AppSec

ModSecurity était configuré avec SecRuleEngine Off — inactif fonctionnellement — mais le module nginx chargeait quand même 11 872 règles OWASP CRS à chaque démarrage. CrowdSec AppSec (appsec-generic-rules + appsec-virtual-patching) couvre le même périmètre avec des règles maintenues communautairement.

Verdict : ModSecurity supprimé, AppSec conservé.

2. APPSEC_FAILURE_ACTION=allow

La config du bouncer nginx avait APPSEC_FAILURE_ACTION=allow — si AppSec échoue à répondre dans le délai imparti (timeout, requête trop volumineuse), la requête passe. Configuré en passthrough : en cas d’échec AppSec, la requête est transmise au backend sans blocage mais sans inspection. C’est ce bug qui causait les 502 sur Hugo : AppSec renvoyait request body exceeded limit et la logique allow créait un état incohérent.

3. mcp-hugo.access.log parsé avec le mauvais parser

Le log JSON du MCP Hugo était lu par le parser nginx standard (format combined). 126 entrées sur 132 ressortaient “unparsed” dans les métriques CrowdSec. Fix : acquisition dédiée avec le parser mcp-oauth.

4. Doublon http-sensitive-files — fausse alerte

Le warning multiple scenarios named crowdsecurity/http-sensitive-files était trompeur : un override local (capacity 2, leakspeed 60s) remplace intentionnellement le scénario hub (capacity 4, leakspeed 5s) pour réduire les faux positifs. Un seul scénario actif, rien à corriger.

5. proxy_buffers nginx surdimensionnés

Trois vhosts (radarr, sonarr, plex) utilisaient proxy_buffers 32 512k ou 16 256k — jusqu’à 16 MB de buffer par connexion en pic. Pour un trafic homelab quasi nul, c’est un gaspillage direct de PSS nginx.

6. nginx-bouncer : 0 décisions non-vides

Les métriques montraient crowdsec-nginx-bouncer : Empty answers = 11 / Non-empty = 0. Comportement normal : les IPs bannies sont bloquées par Cloudflare avant d’atteindre nginx. Le bouncer interroge le LAPI pour chaque visiteur légitime → réponse vide normale. Aucun dysfonctionnement.


🔧 Fixes appliqués

Fix 1 — Suppression ModSecurity

# Commenter le chargement du module dans nginx.conf
#load_module /etc/nginx/modules-enabled/ngx_http_modsecurity_module.so;

# Désactiver dans tous les vhosts (sed sur sites-available/)
#modsecurity on;        # DISABLED — using CrowdSec AppSec only
#modsecurity_rules_file ...;

Appliqué sur 10 vhosts actifs via sed en une passe. nginx -t + systemctl reload nginx.

Fix 2 — APPSEC_FAILURE_ACTION

# /etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf
APPSEC_FAILURE_ACTION=passthrough   # was: allow

Fix 3 — Acquisition dédiée mcp-hugo

# /etc/crowdsec/acquis.d/mcp-hugo.yaml
filenames:
  - /var/log/nginx/mcp-hugo.access.log
labels:
  type: mcp-oauth

Fix 5 — Réduction proxy_buffers

# Avant (radarr, sonarr)
proxy_buffers 32 512k;
proxy_buffer_size 512k;

# Après
proxy_buffers 8 64k;
proxy_buffer_size 16k;
proxy_busy_buffers_size 128k;

📊 Résultats

Mémoire nginx après restart complet

nginx: master    →  PSS  2.3 MB
nginx: worker ×3 →  PSS  6.4 MB chacun
nginx: worker ×1 →  PSS  6.9 MB
Total            →  PSS ~27 MB

Comparatif avant / après

ComposantAvantAprès
nginx (total PSS)~520 MB~27 MB
CrowdSec agent~170 MB~170 MB
firewall-bouncer~30 MB~30 MB
Stack sécurité totale~720 MB~227 MB

-493 MB libérés. Le gain vient intégralement du déchargement du module ModSecurity et de la réduction des proxy_buffers.

Sécurité : aucune régression

CoucheAvantAprès
Edge WAFCloudflare ✔Cloudflare ✔
Inspection applicativeModSec (off) + AppSecAppSec seul ✔
Détection comportementaleCrowdSec ✔CrowdSec ✔
Ban réseaufirewall-bouncer ✔firewall-bouncer ✔
Ban Cloudflare autocf-sync ✔cf-sync ✔

🧠 Leçon

La stack était qualifiée d’“over-secured” — plusieurs couches de WAF empilées, chacune ajoutée à un moment différent pour une raison valide. Le problème : une couche désactivée fonctionnellement reste coûteuse en ressources si le module est chargé. ModSecurity avec SecRuleEngine Off consommait autant de RAM qu’en mode actif — 11 872 règles regex compilées au démarrage nginx, que les requêtes soient inspectées ou non.

La règle à retenir : une couche de sécurité inactive n’est pas gratuite si le module est en mémoire.


🏁 Stack finale

Internet
    ↓
Cloudflare WAF + DDoS
    ↓
nginx (Lua bouncer — stream mode)
    ↓
CrowdSec AppSec  (appsec-generic-rules + appsec-virtual-patching)
    ↓
CrowdSec agent v1.7.8
    ↓
firewall-bouncer (nftables) / nginx-bouncer / cf-sync
    ↓
Backend

Quatre couches actives, zéro redondance, ~500 MB de RAM récupérés.