GUIDE
2026
pour les Nuls
Guide pratique — La Cyber pour les Nuls
WAF Apache
+ ModSecurity
🛡️ ModSecurity v3 📋 OWASP CRS 4.x 🔒 Headers HTTP 🚫 Fail2ban 📊 Rapport quotidien 🔐 CSP
🤓
La Cyber pour les Nuls — Avril 2026
🏗️

Partie A — Architecture : le WAF comme reverse proxy

Le principe fondamental : le WAF intercepte tout le trafic avant qu'il n'atteigne votre application.

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.

Internet WAF (Apache + ModSecurity) Backend ┌────────┐ ┌────────────────────────┐ ┌──────────┐ │ Client │──── HTTPS ───→│ 1. Inspection CRS │── OK ──→│ App Web │ │ │ │ 2. Scoring anomalie │ │ (PHP, │ │ │ │ 3. Headers sécurité │ │ Python, │ │ │←── Réponse ───│ 4. Fail2ban │←────────│ Node…) │ └────────┘ └────────────────────────┘ └──────────┘ │ 403 │ si score ≥ seuil ▼ ┌──────────┐ │ BLOQUÉ │ └──────────┘

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>
N'oubliez pas
Un VirtualHost = un site = des logs séparés. Ne regroupez jamais les logs de plusieurs sites — vous ne pourrez plus analyser les attaques ciblant un site spécifique. Le rapport WAF quotidien (Partie F) s'appuie sur cette séparation.
📋

Partie B — OWASP CRS : les règles de détection

Le cerveau du WAF : 27 fichiers de règles, un scoring par anomalie, et un seuil configurable.

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>
Interdit
Ne chargez jamais deux versions de CRS en même temps. Si vous voyez l'erreur 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ètreValeur par défautSignification
inbound_anomaly_score_threshold5Seuil pour bloquer une requête entrante
outbound_anomaly_score_threshold4Seuil pour bloquer une réponse sortante
paranoia_level1Niveau 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|'"
Truc
Ajoutez 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 :

ModeComportementUsage
SecRuleEngine DetectionOnlyLog les alertes mais ne bloque rienPhase de découverte — analyser les faux positifs
SecRuleEngine OnLog + bloque les requêtes au-dessus du seuilProduction — après avoir traité les faux positifs
N'oubliez pas
Commencez toujours en DetectionOnly pendant 1 à 2 semaines. Analysez les logs, excluez les faux positifs (Partie D), puis passez en On. Basculer directement en blocage, c'est garantir une panne applicative le premier jour.

B.6 Les familles de règles CRS

FichierIDMenace détectée
REQUEST-920920xxxViolations de protocole HTTP
REQUEST-921921xxxAttaques de protocole (HTTP smuggling)
REQUEST-930930xxxLocal File Inclusion (LFI)
REQUEST-931931xxxRemote File Inclusion (RFI)
REQUEST-932932xxxRemote Code Execution (RCE)
REQUEST-933933xxxAttaques PHP
REQUEST-941941xxxCross-Site Scripting (XSS)
REQUEST-942942xxxSQL Injection (SQLi)
REQUEST-943943xxxSession Fixation
REQUEST-944944xxxAttaques Java
RESPONSE-950950xxxFuite de données dans les réponses
RESPONSE-980980xxxCorrélation et scoring final
🔒

Partie C — Headers HTTP de sécurité

Le WAF est l'endroit idéal pour centraliser les headers de sécurité — pas dans chaque application.

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.conf
ServerTokens 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>
Attention
L'ordre est crucial : supprimer d'abord les headers du backend (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

HeaderValeurProtection
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preloadForce HTTPS pendant 1 an, même si l'utilisateur tape http://
X-Content-Type-OptionsnosniffEmpêche le navigateur de deviner le type MIME (MIME sniffing)
X-Frame-OptionsDENYEmpêche l'inclusion du site dans une iframe (clickjacking)
X-XSS-Protection1; mode=blockActive le filtre XSS natif du navigateur
Referrer-Policystrict-origin-when-cross-originLimite les informations envoyées dans le header Referer
Permissions-Policygeolocation=(), 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

Le vrai travail d'un WAF n'est pas de tout bloquer — c'est de ne bloquer que les vrais attaquants.

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>
Truc
Désactivez ModSecurity uniquement sur les routes API internes (LLM, streaming). Les routes publiques (login, pages statiques, .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)

Le header le plus puissant — et le plus complexe à déployer correctement.

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 :

VirtualHost — CSP Report-Only
# 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 :

CSP finale — site statique
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'"
Attention — Google Fonts et RGPD
Si vous utilisez Google Fonts via CDN (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

Un rapport HTML quotidien par mail — sans SIEM, sans ELK, juste un script bash.

F.1 Architecture du rapport

1
📋 Cron exécute le script chaque jour à 7h
2
🔍 Parse error.log (ModSecurity) + access.log (Apache) + fail2ban.log
3
🌍 Géolocalise les IPs attaquantes (GeoIP MaxMind)
4
📧 Envoie un mail HTML enrichi au responsable sécurité

F.2 Prérequis

Bash
sudo apt install -y msmtp geoip-bin geoip-database gawk

F.3 Contenu du rapport

Le rapport quotidien contient :

SectionDonnées
SynthèseNombre de requêtes, alertes ModSecurity, blocages 403, bans Fail2ban
Top IPs bloquéesTop 10 des IPs avec le plus d'alertes, géolocalisées
Top règles CRSTop 10 des règles les plus déclenchées (avec ID et message)
Top URIs cibléesLes chemins les plus attaqués (/.env, /.git, /wp-admin…)
Codes HTTPDistribution des codes de retour (200, 301, 403, 404, 500)
Fail2banIPs 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
Truc
Un log par site + un rapport par site. Si vous avez 5 sites sur le même WAF, vous pouvez exécuter le script 5 fois avec des paramètres différents, ou configurer un seul rapport consolidé. La clé : des logs séparés par VirtualHost.
🐛

Partie G — Les pièges qu'on a évités pour vous

Retour d'expérience sur des déploiements réels.
❌ « Found another rule with the same id » au démarrage Apache
Cause
Deux CRS chargés simultanément : l'ancien (paquet Ubuntu modsecurity-crs) et le nouveau (installé manuellement). Le security2.conf inclut les deux répertoires.
Solution
Nettoyer security2.conf pour ne garder qu'un seul chemin CRS. Utiliser des Include explicites au lieu d'IncludeOptional *.conf.
❌ Application IA bloquée en permanence (55 000 alertes/jour)
Cause
Les conversations LLM contiennent du code, du SQL, du HTML — le CRS détecte des attaques SQLi/XSS dans chaque message. Ce ne sont pas de vraies attaques.
Solution
Désactiver ModSecurity sur les routes API LLM (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.
❌ Headers HSTS en doublon (backend + WAF)
Cause
Le backend (WordPress, Django…) et le WAF posent chacun leur HSTS avec des valeurs différentes. Le navigateur reçoit deux headers.
Solution
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.
❌ CSP qui casse le site en production
Cause
Déploiement direct en enforcement sans phase Report-Only. Google Fonts, analytics, widgets tiers bloqués.
Solution
Toujours commencer par Content-Security-Policy-Report-Only pendant 2-3 semaines. Collecter les violations, ajuster les sources autorisées, puis passer en enforcement.
❌ Rapports CSP bloqués par la règle CRS 920420
Cause
Les navigateurs envoient les rapports CSP en POST avec Content-Type: application/csp-report, qui n'est pas dans la liste CRS des types autorisés.
Solution
Ajouter application/csp-report dans tx.allowed_request_content_type dans crs-setup.conf, ou exclure la règle 920420 sur l'URI /csp-report.
Récapitulatif des commandes essentielles
Tester la config : sudo apache2ctl configtest
Recharger : sudo systemctl reload apache2
Tester 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 ModSecurity
Rapport manuel : sudo /opt/waf-report/waf_daily_report.sh