Le WAF (Web Application Firewall) est le dernier maillon de la chaine reverse-proxy avant les backends applicatifs. Il inspecte chaque requete HTTP entrante et bloque celles qui correspondent a des patterns d'attaque connus. Cet article couvre la configuration du service WAF, ses regles ModSecurity, le Core Rule Set OWASP et les adaptations specifiques au homelab.
Position dans la chaine reverse-proxy
Le WAF s'insere en fin de chaine, apres la terminaison TLS et le routage applicatif :
flowchart LR
A["Client externe"] --> B["HAProxy<br/>TLS termination"]
B --> C["Traefik<br/>HTTPRoute + CrowdSec"]
C --> D["WAF nginx<br/>ModSecurity + CRS"]
D --> E["Backend pod"]
style D fill:#e74c3c,color:#fff
Chaque composant a un role distinct : HAProxy gere le TLS et le load-balancing, Traefik assure le routage par HTTPRoute et applique le middleware CrowdSec (IP reputation), et le WAF effectue l'inspection profonde des requetes HTTP au niveau applicatif.
OWASP Core Rule Set 4.x
Le moteur ModSecurity v3 est couple au OWASP Core Rule Set (CRS) 4.x, un ensemble de regles generiques qui protegent contre les attaques web les plus courantes :
- SQL Injection (SQLi) : detection de payloads SQL dans les parametres, headers et corps de requete
- Cross-Site Scripting (XSS) : identification de code JavaScript injecte dans les inputs
- Remote Code Execution (RCE) : blocage des tentatives d'execution de commandes systeme
- Local File Inclusion (LFI) : prevention de la lecture de fichiers sensibles via path traversal
- Remote File Inclusion (RFI) : blocage du chargement de ressources externes malveillantes
Le CRS fonctionne en mode anomaly scoring : chaque regle qui matche ajoute des points a un score. Si le score depasse le seuil configure (par defaut 5 en mode paranoia 1), la requete est bloquee avec un code 403.
Configuration par site
Chaque domaine heberge derriere le WAF dispose de son propre fichier de configuration dans conf.d/ :
# conf.d/jbsky.conf
server {
listen 8443;
server_name jbsky.fr;
modsecurity on;
modsecurity_rules_file /etc/nginx/modsecurity/modsecurity.conf;
location / {
proxy_pass http://wordpress-svc:80;
}
location = /healthz {
auth_basic off;
modsecurity off;
return 200 'ok';
}
}
Cette approche permet des exceptions granulaires par site sans impacter les autres domaines.
Limite de taille des requetes
Le parametre SecRequestBodyLimit est fixe a 10 MB au niveau du connecteur ModSecurity. C'est une limite hard : elle est appliquee par le module avant meme que nginx traite la requete. La directive ctl:requestBodyAccess=Off dans une regle CRS ne permet pas de la contourner -- le connecteur rejette le corps de requete avant que les regles soient evaluees.
Pour les applications qui necessitent des uploads volumineux (comme Immich pour les photos), la solution est de desactiver completement ModSecurity sur les locations d'upload :
# conf.d/immich.conf - exception pour uploads binaires
server {
listen 8443;
server_name photos.jbsky.fr;
modsecurity on;
modsecurity_rules_file /etc/nginx/modsecurity/modsecurity.conf;
location /api/assets {
modsecurity off; # uploads binaires, pas de WAF
proxy_pass http://immich-server:2283;
client_max_body_size 50G;
}
location / {
proxy_pass http://immich-server:2283;
}
}
Le piege du default_server
nginx sans directive default_server explicite utilise le premier server block charge alphabetiquement comme serveur par defaut. Les health probes Kubernetes (liveness/readiness) envoient des requetes avec un header Host generique qui ne correspond a aucun server_name configure.
flowchart TD
A["Probe K8s<br/>Host: waf.jbsky.fr"] --> B{"server_name match ?"}
B -- "Aucun match" --> C["Premier server block<br/>alphabetique = default"]
C --> D{"auth_basic active ?"}
D -- "Oui" --> E["HTTP 401<br/>Probe echoue"]
D -- "Non" --> F["HTTP 200<br/>Probe OK"]
E --> G["CrashLoopBackOff<br/>Pod instable"]
style E fill:#e74c3c,color:#fff
style G fill:#c0392b,color:#fff
Si le server block qui devient default implicite a auth_basic active, les probes recoivent un 401, le pod entre en CrashLoopBackOff, et l'ancien pod continue avec une configuration nginx stale. Regle absolue : tout server block avec auth_basic doit avoir un location = /healthz avec auth_basic off et modsecurity off.
Deploiement K3s
Le WAF est deploye dans le namespace waf du cluster K3s via un Helm chart. La configuration specifique est injectee par ConfigMap overlay :
- Les fichiers
conf.d/*.confsont montes comme ConfigMap dans le pod WAF - La configuration ModSecurity de base est embarquee dans l'image
- Les overrides par site sont dans des ConfigMaps separees
Gotcha important : les ConfigMaps montees en volume sont automatiquement mises a jour par kubelet (~1 minute), mais nginx ne relit pas sa configuration sans reload. Le fichier .conf apparait bien sur le filesystem du pod, mais le processus nginx runtime l'ignore. Un rollout restart du deployment est obligatoire pour que nginx charge la nouvelle configuration.
Flux d'inspection des requetes
flowchart TD
A["Requete HTTP entrante"] --> B["Phase Request Headers"]
B --> C{"Headers suspects ?"}
C -- "Score eleve" --> D["Blocage 403"]
C -- "Score faible" --> E["Phase Request Body"]
E --> F{"Body > SecRequestBodyLimit ?"}
F -- "Oui" --> D
F -- "Non" --> G{"Payload malveillant ?"}
G -- "SQLi / XSS / RCE" --> D
G -- "Legitime" --> H["Phase Response Headers"]
H --> I["Phase Response Body"]
I --> J{"Fuite de donnees ?"}
J -- "Oui" --> D
J -- "Non" --> K["Reponse au client"]
style D fill:#e74c3c,color:#fff
style K fill:#27ae60,color:#fff
Articles lies
- Image Docker WAF nginx ModSecurity : construction de l'image hardened FROM scratch
- Reverse proxy HAProxy + Traefik + WAF : architecture complete de la chaine reverse-proxy
Laisser un commentaire