Partie A — Architecture : le WAF comme reverse proxy
A.1 Le rôle du WAF
Un WAF (Web Application Firewall) est un reverse proxy Apache qui inspecte chaque requête HTTP avant de la transmettre au backend. Il détecte et bloque les attaques web (SQLi, XSS, LFI, RCE) en analysant les URL, les headers, les paramètres et le corps des requêtes.
A.2 Structure d'un VirtualHost WAF
VirtualHost type<VirtualHost *:443>
ServerName www.example.com
Protocols h2 http/1.1
# Sécurité de base
TraceEnable off
ServerSignature Off
# Logs séparés par site
ErrorLog ${APACHE_LOG_DIR}/example_error.log
CustomLog ${APACHE_LOG_DIR}/example_access.log combined
# Headers de sécurité (cf. Partie C)
# ... (détaillé plus bas)
# Reverse proxy vers le backend
ProxyPass / http://192.168.x.x:8080/
ProxyPassReverse / http://192.168.x.x:8080/
# TLS
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
</VirtualHost>
Partie B — OWASP CRS : les règles de détection
B.1 Installer ModSecurity + CRS
Ubuntu 22.04 / 24.04# Installer ModSecurity pour Apache
sudo apt install libapache2-mod-security2
# Activer le module
sudo a2enmod security2
# Installer le CRS (dernière version)
cd /etc/crs
sudo wget https://github.com/coreruleset/coreruleset/archive/refs/tags/v4.10.0.tar.gz
sudo tar xzf v4.10.0.tar.gz
sudo ln -s coreruleset-4.10.0 current
B.2 Configurer le chargement des règles
/etc/apache2/mods-enabled/security2.conf<IfModule security2_module>
SecDataDir /var/cache/modsecurity
Include /etc/modsecurity/modsecurity.conf
Include /etc/modsecurity/crs-setup.conf
Include /etc/crs/current/rules/*.conf
</IfModule>
Found another rule with the same id, c'est que deux CRS coexistent (l'ancien du paquet Ubuntu + le nouveau). Nettoyez security2.conf pour ne garder qu'un seul chemin vers les règles.
B.3 Le scoring par anomalie
Le CRS ne bloque jamais une requête sur une seule règle. Chaque règle déclenchée ajoute un score. Si le total dépasse le seuil, la requête est bloquée. C'est le mode Anomaly Scoring.
| Paramètre | Valeur par défaut | Signification |
|---|---|---|
inbound_anomaly_score_threshold | 5 | Seuil pour bloquer une requête entrante |
outbound_anomaly_score_threshold | 4 | Seuil pour bloquer une réponse sortante |
paranoia_level | 1 | Niveau de sensibilité (1 = standard, 4 = maximum) |
Un score typique pour une attaque SQLi : 5 points (une seule règle 942xxx suffit). Pour une requête légitime : 0 points. Les faux positifs ajoutent souvent 3 points — en dessous du seuil.
B.4 Le fichier crs-setup.conf
/etc/modsecurity/crs-setup.conf# Mode de détection : On = bloque, DetectionOnly = log uniquement
SecRuleEngine On
# Scoring
SecAction "id:900110,phase:1,pass,nolog,\
setvar:tx.inbound_anomaly_score_threshold=5,\
setvar:tx.outbound_anomaly_score_threshold=4"
# Paranoia level (1 = standard, raisonnable pour la plupart des sites)
SecAction "id:900000,phase:1,pass,nolog,\
setvar:tx.paranoia_level=1"
# Contenu autorisé
SecAction "id:900220,phase:1,pass,nolog,\
setvar:'tx.allowed_request_content_type=|application/x-www-form-urlencoded|\
|multipart/form-data||text/xml||application/xml||application/soap+xml|\
|application/json||application/cloudevents+json||application/csp-report|'"
application/csp-report dans allowed_request_content_type si vous utilisez une CSP avec collecte des violations (Partie E). Sinon la règle 920420 bloquera les rapports envoyés par les navigateurs.
B.5 DetectionOnly vs On
Le mode DetectionOnly est indispensable pour les premiers jours de déploiement :
| Mode | Comportement | Usage |
|---|---|---|
SecRuleEngine DetectionOnly | Log les alertes mais ne bloque rien | Phase de découverte — analyser les faux positifs |
SecRuleEngine On | Log + bloque les requêtes au-dessus du seuil | Production — après avoir traité les faux positifs |
On. Basculer directement en blocage, c'est garantir une panne applicative le premier jour.
B.6 Les familles de règles CRS
| Fichier | ID | Menace détectée |
|---|---|---|
| REQUEST-920 | 920xxx | Violations de protocole HTTP |
| REQUEST-921 | 921xxx | Attaques de protocole (HTTP smuggling) |
| REQUEST-930 | 930xxx | Local File Inclusion (LFI) |
| REQUEST-931 | 931xxx | Remote File Inclusion (RFI) |
| REQUEST-932 | 932xxx | Remote Code Execution (RCE) |
| REQUEST-933 | 933xxx | Attaques PHP |
| REQUEST-941 | 941xxx | Cross-Site Scripting (XSS) |
| REQUEST-942 | 942xxx | SQL Injection (SQLi) |
| REQUEST-943 | 943xxx | Session Fixation |
| REQUEST-944 | 944xxx | Attaques Java |
| RESPONSE-950 | 950xxx | Fuite de données dans les réponses |
| RESPONSE-980 | 980xxx | Corrélation et scoring final |
Partie C — Headers HTTP de sécurité
C.1 Pourquoi centraliser au WAF ?
Les applications backend (WordPress, Node.js, Python/Django…) ont chacune leur propre gestion des headers. Certaines les oublient, d'autres les doublent. En les posant au niveau du WAF, vous garantissez une politique uniforme sur tous les sites.
C.2 Configuration globale — security.conf
Ce fichier est chargé par tous les VirtualHosts :
/etc/apache2/conf-available/security.confServerTokens Prod
ServerSignature Off
TraceEnable Off
# Bloquer l'accès aux fichiers sensibles
RedirectMatch 404 /\.git
RedirectMatch 404 /\.svn
RedirectMatch 404 /\.env
<IfModule mod_headers.c>
# Supprimer les headers remontés par le backend
Header onsuccess unset X-Content-Type-Options
Header onsuccess unset X-Frame-Options
Header onsuccess unset Strict-Transport-Security
Header onsuccess unset Server
Header always unset X-Content-Type-Options
Header always unset X-Frame-Options
Header always unset Strict-Transport-Security
Header always unset Server
# Poser NOS headers de sécurité
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Header always set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
Header always set Pragma "no-cache"
Header always set Server "Apache"
</IfModule>
onsuccess unset + always unset), puis poser les nôtres (always set). Sinon vous obtenez des doublons (ex : deux Strict-Transport-Security avec des valeurs différentes).
C.3 Les headers expliqués
| Header | Valeur | Protection |
|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains; preload | Force HTTPS pendant 1 an, même si l'utilisateur tape http:// |
X-Content-Type-Options | nosniff | Empêche le navigateur de deviner le type MIME (MIME sniffing) |
X-Frame-Options | DENY | Empêche l'inclusion du site dans une iframe (clickjacking) |
X-XSS-Protection | 1; mode=block | Active le filtre XSS natif du navigateur |
Referrer-Policy | strict-origin-when-cross-origin | Limite les informations envoyées dans le header Referer |
Permissions-Policy | geolocation=(), microphone=(), camera=() | Désactive les API sensibles du navigateur |
C.4 Cas particulier : sites WordPress avec ressources externes
Pour les sites avec Google Fonts, analytics, plugins tiers : ne posez pas de CSP dans un premier temps. Les autres headers suffisent pour satisfaire les audits RiskRecon/Bitsight. La CSP est un chantier à part (cf. Partie E).
Partie D — Gérer les faux positifs
D.1 Identifier un faux positif
Dans les logs ModSecurity, un faux positif ressemble à ça :
error.log[client 10.0.0.5] ModSecurity: Warning.
Pattern match "(?:select|union|insert|update|delete)" at ARGS:query.
[id "942100"] [msg "SQL Injection Attack Detected"]
[severity "CRITICAL"] [tag "attack-sqli"]
[hostname "app.example.com"] [uri "/api/search"]
La clé : l'URI /api/search avec un paramètre query est une fonctionnalité légitime de votre application. La règle 942100 voit le mot select dans le paramètre et crie à l'injection SQL.
D.2 Exclure une règle sur une URI spécifique
Exclusion ciblée# Exclure la règle 942100 uniquement sur /api/search
SecRule REQUEST_URI "@beginsWith /api/search" \
"id:9000001,phase:1,pass,nolog,\
ctl:ruleRemoveById=942100"
D.3 Exclure une règle sur un paramètre spécifique
Exclusion par paramètre# Ne pas inspecter le paramètre 'content' (éditeur WYSIWYG)
SecRuleUpdateTargetById 942100 "!ARGS:content"
SecRuleUpdateTargetById 941100 "!ARGS:content"
D.4 Exclusions pour les applications IA / LLM
Les applications d'IA (OpenWebUI, ChatGPT, Ollama) génèrent un volume massif de faux positifs car les conversations contiennent du code, du SQL, du HTML — tout ce que le CRS cherche à bloquer.
Exclusion routes API LLM# Routes API streaming LLM — désactiver ModSecurity
<LocationMatch "^/(api/v1/chats|ollama|litellm)">
SecRuleEngine Off
</LocationMatch>
.env, .git) restent protégées. La sécurité d'une app IA repose sur le contrôle d'accès (Azure AD, IP whitelist) + Fail2ban, pas sur le CRS.
Partie E — Content Security Policy (CSP)
E.1 Le principe
La CSP dit au navigateur : « voici les seules sources autorisées pour charger des scripts, styles, images, polices, etc. ». Tout ce qui n'est pas dans la liste est bloqué par le navigateur — même si le HTML de la page le demande. C'est la protection ultime contre le XSS.
E.2 Phase 1 — Report-Only (découverte)
Ne déployez jamais une CSP en enforcement sans phase de découverte. Utilisez Content-Security-Policy-Report-Only pour observer les violations sans rien bloquer :
# Phase découverte — log uniquement, ne bloque rien
Header always set Content-Security-Policy-Report-Only "\
default-src 'self'; \
script-src 'self'; \
style-src 'self' 'unsafe-inline'; \
img-src 'self' data:; \
font-src 'self'; \
connect-src 'self'; \
frame-ancestors 'none'; \
form-action 'self'; \
base-uri 'self'; \
object-src 'none'; \
report-uri /csp-report"
E.3 Collecter les violations avec ModSecurity
Plutôt que de déployer un endpoint PHP/Python, exploitez ModSecurity comme collecteur CSP :
Règle ModSecurity# Collecteur CSP — exclure le Content-Type inhabituel du CRS
SecRule REQUEST_URI "@streq /csp-report" \
"id:9000099,phase:1,pass,nolog,\
ctl:ruleRemoveById=920420"
E.4 Phase 2 — Enforcement
Après 2-3 semaines de collecte et d'ajustement (ajout des domaines tiers détectés dans les violations), remplacez Report-Only par Content-Security-Policy :
Header always set Content-Security-Policy "\
default-src 'self'; \
script-src 'self'; \
style-src 'self' 'unsafe-inline'; \
img-src 'self' data:; \
font-src 'self'; \
connect-src 'self'; \
frame-ancestors 'none'; \
object-src 'none'"
fonts.googleapis.com), chaque chargement envoie l'IP du visiteur à Google. Jurisprudence allemande (LG München, 2022) : condamnation. Hébergez les polices localement plutôt que d'ajouter Google dans votre CSP.
Partie F — Rapport WAF quotidien + Fail2ban
F.1 Architecture du rapport
error.log (ModSecurity) + access.log (Apache) + fail2ban.logF.2 Prérequis
Bashsudo apt install -y msmtp geoip-bin geoip-database gawk
F.3 Contenu du rapport
Le rapport quotidien contient :
| Section | Données |
|---|---|
| Synthèse | Nombre de requêtes, alertes ModSecurity, blocages 403, bans Fail2ban |
| Top IPs bloquées | Top 10 des IPs avec le plus d'alertes, géolocalisées |
| Top règles CRS | Top 10 des règles les plus déclenchées (avec ID et message) |
| Top URIs ciblées | Les chemins les plus attaqués (/.env, /.git, /wp-admin…) |
| Codes HTTP | Distribution des codes de retour (200, 301, 403, 404, 500) |
| Fail2ban | IPs bannies, jails déclenchés, durée des bans |
F.4 Crontab
Crontab# Rapport WAF quotidien à 7h00
0 7 * * * root /opt/waf-report/waf_daily_report.sh >> /var/log/waf_report.log 2>&1
Partie G — Les pièges qu'on a évités pour vous
modsecurity-crs) et le nouveau (installé manuellement). Le security2.conf inclut les deux répertoires.
security2.conf pour ne garder qu'un seul chemin CRS. Utiliser des Include explicites au lieu d'IncludeOptional *.conf.
SecRuleEngine Off dans un LocationMatch). La sécurité repose sur le contrôle d'accès (Azure AD + IP), pas sur l'inspection CRS du contenu.
Header onsuccess unset Strict-Transport-Security + Header always unset Strict-Transport-Security avant de poser le nôtre. L'ordre unset puis set est critique.
Content-Security-Policy-Report-Only pendant 2-3 semaines. Collecter les violations, ajuster les sources autorisées, puis passer en enforcement.
POST avec Content-Type: application/csp-report, qui n'est pas dans la liste CRS des types autorisés.
application/csp-report dans tx.allowed_request_content_type dans crs-setup.conf, ou exclure la règle 920420 sur l'URI /csp-report.
sudo apache2ctl configtestRecharger :
sudo systemctl reload apache2Tester SQLi :
curl -sk "https://site/?id=1+UNION+SELECT+1,2--" (doit retourner 403)Tester XSS :
curl -sk "https://site/?x=<script>alert(1)</script>" (doit retourner 403)Tester .env :
curl -sk "https://site/.env" (doit retourner 404)Vérifier headers :
curl -sI https://site/ | grep -iE "strict|x-frame|x-content|referrer|permissions"Logs temps réel :
tail -f /var/log/apache2/site_error.log | grep ModSecurityRapport manuel :
sudo /opt/waf-report/waf_daily_report.sh