Skip to content
github-actions[bot] edited this page Oct 28, 2026 · 7 revisions

WAF (Web Application Firewall)

English · 🌐 Français

Arenet embarque Coraza v3.7 — un moteur WAF Go-native compatible avec le format de règles ModSecurity — pré-chargé avec OWASP CRS v4.25 (Core Rule Set). Chaque route peut opt-in à la protection WAF à l'un des trois niveaux :

Mode Comportement
off Handler WAF pas dans la chain. Zéro overhead.
detect Le WAF évalue chaque requête, émet un waf_event dans /security pour chaque match, mais laisse la requête continuer vers l'upstream. À utiliser pour observation / triage.
block Le WAF évalue, émet l'événement, et rejette la requête avec 403 Forbidden quand le score d'anomalie dépasse le threshold de blocage.

Quick start : activer WAF sur une route

  1. Sidebar → Routes
  2. Clique ta route → Edit
  3. WAF mode → choisis detect (recommandé pour le premier déploiement)
  4. Save

En 5 secondes le WAF est actif. Envoie un probe pour tester :

curl "https://your-route.example.com/?id=1' OR '1'='1"
# En detect mode : la requête atteint ton backend ET un événement SQLi land dans /security
# En block mode : 403 Forbidden, la requête n'atteint pas le backend

Ouvre /security dans l'UI : tu verras l'événement listé avec catégorie SQLi, l'ID de règle CRS qui a matché (942100 pour libinjection), IP source + GeoIP, l'échantillon de payload qui a matché.


Anatomie d'un événement WAF

Chaque match émet une ligne dans la table SQLite waf_event avec :

  • ts — timestamp
  • route_id — quelle route a triggered
  • rule_id — ID de règle CRS (ex. 942100, 920170)
  • category — classification Arenet (SQLi / XSS / RCE / LFI / RFI / PROTOCOL / METHOD / SCANNER / SESSION / DATA_LEAK / ...) — dérivée du range d'ID de règle, voir internal/waf/category.go
  • severity — CRITICAL / ERROR / WARNING / NOTICE
  • src_ip — IP distante
  • request_path — {method} {uri}
  • payload_sample — premiers 256 chars de la data qui a matché
  • action — BLOCK (block mode + score ≥ threshold) ou DETECT (detect mode OU block mode + score < threshold)
  • status_code — 403 pour blocked, 200 pour detect/observed
  • matched_var (v2.36+) — le champ qui a déclenché la règle, sous la forme VARIABLE:nom (ex. ARGS:content, REQUEST_HEADERS:referer). Vide pour les événements enregistrés avant la v2.36 et pour les règles qui regardent l'URL elle-même.

La page Journaux et le détail /security/<routeId> listent ces événements. Pour les admins, chaque événement excluable a un bouton Exclure… (voir Exclusion ciblée plus bas).

Les répétitions d'un même trio route + IP source + règle dans les 60 s ne sont enregistrées qu'une fois (les compteurs du tableau de bord comptent quand même chaque requête).


Gérer les faux positifs

Quand le WAF bloque une requête légitime (un faux positif connu), tu as quatre portes de sortie à appliquer par route, de la plus étroite à la plus large :

Option (0) — Exclusion ciblée depuis un événement (recommandé)

À utiliser quand : une règle se déclenche sur un champ légitime précis — le paramètre content d'un éditeur de texte qui déclenche la règle SQLi 942100, un en-tête Referer long qui déclenche une règle XSS…

  1. Ouvre Journaux (ou la page Sécurité de la route) et repère l'événement.
  2. Clique sur Exclure…. La fenêtre dit en clair ce qui va se passer : « La règle 942100 n'inspectera plus le paramètre « content ». Elle continue de protéger tous les autres champs de la route. »
  3. Seulement sur ce chemin est coché par défaut, avec le chemin de l'événement (ex. /api/save). Modifie-le si besoin, coche et tout ce qui est en dessous pour toute une arborescence (/api couvre /api et /api/…, pas /apix), ou décoche pour couvrir tous les chemins de la route.
  4. Exclure. Caddy recharge ; la même requête passe, alors que la même charge dans un autre champ, ou sur un autre chemin, reste inspectée.

C'est la méthode recommandée par OWASP : la règle reste active, seul ce champ est ignoré (ctl:ruleRemoveTargetById).

À savoir :

  • Quand l'événement n'indique pas le champ (événements enregistrés avant la v2.36, ou règle qui inspecte l'URL elle-même), la fenêtre te prévient et exclut la règle sur toute la route (comme l'option (c)).
  • Les règles d'évaluation du blocage du CRS (949xxx, 959xxx), d'initialisation (901xxx) et de corrélation (980xxx) n'ont pas de bouton : les exclure désactiverait le blocage lui-même. Exclus plutôt la règle listée à côté (ex. 942100). Les règles propres à Arenet (1xxxxx) n'en ont pas non plus.
  • La section WAF du formulaire de route liste les exclusions ciblées (Exclusions ciblées), une ligne lisible chacune, avec Supprimer et un petit formulaire pour en ajouter une à la main (ID de règle, type de champ, nom du champ, chemin optionnel).
  • Réservé aux admins. Chaque ajout est inscrit au journal d'audit comme une modification de route.

Option (a) — Désactiver CRS entièrement sur la route

À utiliser quand : la route sert du trafic interne de confiance (outil d'admin sur LAN), ou le backend est tellement bruyant que CRS produit 90% de faux positifs.

  1. Édite la route → section WAF → Disable OWASP CRS on this route ✅
  2. Un dialogue de confirmation (ADR D4) prévient des implications sécurité
  3. Save

Le handler WAF reste dans la chain (le dashboard compte toujours les événements à 0), mais les includes CRS sont strippés. Coût d'évaluation CRS à zéro.

Option (c) — Exclure des IDs de règles CRS spécifiques

À utiliser quand : une règle précise (ex. 942100 SQLi libinjection) fire sur un payload légitime dans beaucoup de champs différents — sinon préfère l'option (0).

  1. Édite la route → WAF exclusions → Excluded rule IDs
  2. Liste séparée par virgules : 942100, 920170, 911100
  3. Save

La règle est retirée de l'évaluation CRS pour cette route. Les autres règles de la même famille s'appliquent toujours.

Option (e) — Exclure des familles de tags CRS entières

À utiliser quand : une famille de règles entière est bruyante sur cette route. Exemple : attack-protocol (range de règles 911-921) trip sur l'usage légitime de WebDAV / méthodes HTTP non-standard.

  1. Édite la route → WAF exclusions → Excluded tags
  2. Choisis dans le dropdown autocomplete : attack-protocol, attack-sqli, attack-rce, language-php, paranoia-level/3, ...
  3. Save

Toute la famille de tags est exclue. Les règles avec ce tag sont retirées de l'évaluation CRS pour cette route. Plus cheap à maintenir que de lister 15 IDs de règles.


Règles personnalisées (guidées)

Bloque les requêtes qui correspondent à tes propres critères, sans écrire de SecLang. Formulaire de route → section WAF → Règles personnalisées.

Une règle a un nom et 1 à 8 conditions qui doivent toutes correspondre. Chaque condition accepte plusieurs valeurs (une par ligne — l'une ou l'autre suffit) :

Critère Opérateurs Exemple
Chemin commence par · est · contient commence par /wp-admin
Méthode est · n'est pas n'est pas GET / HEAD / POST
User-Agent contient · est · est absent ou vide contient sqlmap / nikto (sans tenir compte des majuscules)
En-tête (par nom) est absent · est présent · contient · est X-Admin-Token est absent

L'éditeur affiche la règle en phrase pendant la saisie (« Bloquer si la méthode est POST et le chemin commence par /login et le User-Agent est absent ou vide »). Trois modèles la pré-remplissent : Fichiers sensibles (/.env, /.git…), Scanners connus (User-Agents), Méthodes limitées.

Fonctionnement :

  • Une règle suit le mode WAF de la route : Block → 403, Detect → la requête passe et la correspondance est journalisée. Les règles n'ont pas d'effet quand le WAF est off (elles sont conservées).
  • Les règles personnalisées passent avant l'OWASP CRS et fonctionnent aussi quand le CRS est désactivé sur la route.
  • Chaque règle peut être désactivée sans être supprimée. Les règles sont enregistrées avec la route (Enregistrer).
  • Dans l'historique WAF (Journaux, page Sécurité de la route), les événements ont la catégorie CUSTOM et le nom de la règle. Les IDs de règles sont 120000–129999 et ne changent pas quand tu modifies une règle.
  • L'IP source / le réseau n'est pas un critère : utilise pour cela le filtrage IP des règles par chemin.

SecLang (avancé)

Pour ce que les règles guidées ne savent pas exprimer, chaque route a un éditeur SecLang (formulaire de route → WAF → SecLang (avancé), replié tant qu'il est vide). C'est la vraie syntaxe Coraza / ModSecurity, avec des garde-fous et de l'aide :

  • Éditeur : coloration, autocomplétion (directives, variables, @opérateurs, actions, valeurs de ctl: et t: — uniquement ce qui est autorisé), vérification en direct avec le problème affiché sur sa ligne.
  • Modèles, commentés ligne par ligne dans ta langue, insérés avec le prochain ID libre : API en JSON uniquement, méthodes autorisées, /admin depuis le réseau local seulement, exiger un en-tête de clé API, bloquer des robots par User-Agent, limiter la longueur des paramètres, exception pour un champ.
  • → SecLang sur une règle guidée : la remplace par le SecLang qu'elle génère (bonne façon d'apprendre, puis d'aller plus loin).
  • Tester une requête (routes enregistrées) : méthode, chemin, en-têtes, corps → « Bloquée (405) par la règle 130000 Méthode non autorisée » ou « Acceptée », avec toutes les règles déclenchées (OWASP CRS compris). Utilise le contenu non enregistré de l'éditeur, n'envoie rien au backend et ne journalise rien.

Ce qui est autorisé (vérifié à l'enregistrement ; un refus indique la ligne) :

  • Directives SecRule, SecAction, SecMarker uniquement — pas d'Include, pas de réglage du moteur (SecRuleEngine, SecRequestBodyLimit…), pas de journal d'audit, pas de fichiers de données.
  • IDs de règles 130000–139999, un par règle (les maillons d'une chaîne n'en ont pas), uniques.
  • Opérateurs : @rx @streq @beginsWith @endsWith @contains @pm @within @eq @ge @gt @le @lt @ipMatch @detectSQLi @detectXSS @validateByteRange @validateUrlEncoding @validateUtf8Encoding @strmatch @unconditionalMatch @noMatch. Refusés entre autres : @inspectFile (lance un programme), *FromFile (lit des fichiers de l'hôte), @rbl (réseau), @geoLookup, @validateSchema.
  • Actions : id phase chain deny block drop pass allow status redirect log nolog auditlog noauditlog msg logdata tag severity rev ver maturity capture multiMatch setvar expirevar skip skipAfter t ctl. Refusées : exec, setenv, initcol. ctl: limité à la famille ruleRemove…, requestBodyProcessor et forceRequestBodyVariable (pas de ctl:ruleEngine, pas de changement des limites de corps).
  • La collection ENV et les macros %{ENV.…} sont refusées (l'environnement du processus peut contenir des secrets).
  • 64 Kio et 200 règles maximum.

Le SecLang s'exécute après les exclusions ciblées et les règles guidées et avant le CRS, suit le mode de la route (la ligne SecRuleEngine reste celle d'Arenet) et fonctionne aussi quand le CRS est désactivé. Les événements ont la catégorie CUSTOM et le msg de la règle comme nom. Préfère les règles guidées quand elles suffisent : elles ne peuvent pas être mal écrites.


Virtual-patcher des CVE amont (exemple concret)

Arenet est un simple binaire Go — il n'exécute ni PHP ni Node, donc ce type de CVE d'interpréteur ne touche jamais Arenet lui-même. Mais en tant que reverse proxy devant tes applis, il peut en virtual-patcher certaines pour les backends situés derrière, le temps que tu mettes le backend à jour. C'est de la défense en profondeur, pas un remplacement du correctif de l'appli vulnérable.

Deux cas réels de la vague « Black October » d'octobre 2026. Colle la règle dans l'éditeur SecLang (avancé) d'une route, démarre la route en detect, confirme avec Tester une requête, puis passe en block. Les règles sont par route : limite-les aux routes qui exposent réellement l'appli concernée.

Node.js « Node2Shell » — CVE-2026-111497 (efficace)

Écriture hors limites dans le décodeur Brotli embarqué de Node : un seul corps de requête encodé en Brotli qui atteint le décodeur (Express 5 / body-parser 2.x décodent Content-Encoding: br par défaut, donc express.json() suffit) donne une RCE. Les clients n'encodent quasiment jamais les corps de requête en Brotli, donc les rejeter en bordure supprime la surface d'attaque entrante avec un impact négligeable :

SecRule REQUEST_HEADERS:Content-Encoding "@rx (?i)(^|[,\s])br([,;\s]|$)" "id:130201,phase:1,deny,status:415,log,msg:'Block Brotli-encoded request body (Node2Shell CVE-2026-111497)',tag:'cve-2026-111497'"

Rejette Content-Encoding: br (et gzip, br) en phase 1, avant la lecture du corps, avec un 415. Limite : ne couvre que les corps de requête entrants — il ne protège pas le contenu que ton appli va elle-même chercher et décode en Brotli depuis un serveur externe (fetch() / undici). Mets Node à jour en 22.23.4 / 24.22.1 / 26.12.1.

PHP « PHP2Shell » — CVE-2026-109618 / 109619 (partiel — simple ralentisseur)

Bugs de corruption mémoire dans le cœur de l'interpréteur PHP (un use-after-free atteint via session.upload_progress de PHP, plus un chemin out-of-memory), les deux en RCE non authentifiée. Le bug se situe sous le userland, donc une signature WAF ne peut pas être un correctif fiable — elle ne fait qu'élever la barre face au chemin d'exploitation public connu, qui s'appuie sur un champ multipart nommé PHP_SESSION_UPLOAD_PROGRESS :

SecRule ARGS_NAMES "@rx (?i)PHP_SESSION_UPLOAD_PROGRESS" "id:130202,phase:2,deny,status:403,log,msg:'Block PHP session.upload_progress abuse (PHP2Shell CVE-2026-109619)',tag:'cve-2026-109619'"

À lire avant de déployer :

  • Cela bloque une technique d'exploitation, pas la vulnérabilité. CVE-2026-109618 (chemin OOM) n'a pas de signature de requête fiable ; le seul levier brut y est une limite de taille du corps sur l'endpoint d'upload.
  • PHP_SESSION_UPLOAD_PROGRESS est une fonctionnalité légitime de PHP. Si une appli derrière la route utilise réellement l'upload progress, cette règle casse ces uploads — d'où le périmètre par route et le detect d'abord.
  • Rien ici ne remplace la mise à jour de PHP vers la version corrigée.

Les deux règles ont été validées contre l'allowlist SecLang d'Arenet et le moteur Coraza : elles passent la vérification de l'éditeur, bloquent la forme d'attaque, et laissent passer une requête normale. Lance Tester une requête sur ta route pour le constater avant de passer en block.


CRS paranoia levels

OWASP CRS supporte quatre paranoia levels (PL1–PL4) qui contrôlent la strictness :

  • PL1 (défaut) : règles avec faible taux de faux positifs, adapté à la plupart des web apps publics
  • PL2 : ajoute des règles qui peuvent tripper sur des edge cases légitimes
  • PL3 : agressif, attends-toi à des FPs sur des apps à contenu riche
  • PL4 : maximum, pour endpoints hardenés uniquement

Arenet émet actuellement PL1 par défaut pour chaque route. La configuration paranoia level par route n'est pas exposée dans l'UI dès v2.9.3 ; si tu as besoin de PL2+ sur une route spécifique, le workaround est des directives Coraza custom via la config WAF de la route (avancé, pas exposé UI).


Considérations de performance

  • Le handler WAF tourne avant le handler reverse-proxy donc un rejet block-mode n'atteint jamais l'upstream
  • Les instances WAF sont poolées par leur SHA de directives-string — N routes avec une config WAF identique partagent une seule instance Coraza WAF en mémoire (voir la clé de pool dans internal/waf/module.go)
  • L'inspection de body (multipart, XML, JSON) est le chemin le plus lourd. Pour les routes qui handle des gros uploads (serveurs de fichiers, Docker registry), active le Upload streaming mode sur la route qui skip l'inspection de body et demande à Caddy de flush les bytes sans buffering RAM
  • Le moteur Coraza lui-même est Go-native (pas de cgo), donc pas d'overhead de thread par requête

Patterns communs de faux positifs + fixes

Symptôme Cause probable Fix
GET avec body retourne 403 sur API REST 920170 (attack-protocol) Exclure règle 920170 ou tag attack-protocol
PUT/DELETE retourne 403 sur API admin 911100 (METHOD-ENFORCEMENT) Exclure règle 911100 ou tag attack-protocol
Tout ce qui contient -- dans l'URL retourne 403 942100 (SQLi libinjection FP sur UUIDs / version strings) Exclure règle 942100 ou, plus étroit, exclure uniquement sur la route spécifique
Upload multipart retourne 403 922xxx (attaques multipart) Exclure tag attack-multipart ou activer Upload Streaming Mode
Authentik / Keycloak /api/v1/auth/oidc/callback retourne 403 Plusieurs règles CRS tripent sur les params OAuth state/code Exclure tags attack-protocol + attack-sqli sur la route IdP, OU set le WAF de la route IdP à off

Monitoring : rester en avant des faux positifs

Le workflow recommandé pour ajouter une nouvelle route :

  1. Set WAF mode à detect d'abord
  2. Utilise la route normalement pendant 24-48h (ton vrai trafic exercise le set de règles CRS)
  3. Ouvre /security/<routeId> → review les événements générés
  4. Identifie les FPs : événements où le payload qui a matché est de la donnée user légitime, pas une attaque
  5. Ajoute les exclusions (IDs de règles ou tags) pour les FPs
  6. Switch WAF mode à block
  7. Câble une alerte (Alerting) sur waf_event_rate > 10 / 5min pour catcher les spikes inattendus

Observabilité cross-route

Le dashboard /dashboard montre les taux WAF detect/block par top-N routes. La page /security agrège les événements à travers toutes les routes avec filtres. Le /logs unifié montre les événements WAF à côté des événements auth + rate-limit + cert pour corrélation ("est-ce que cette IP a tenté N probes WAF ET échoué auth N fois ET trip rate-limit ?").


See also

  • Routes — où trouver les settings WAF dans l'UI
  • CrowdSec — bannit les IPs WAF-triggering les plus égregious à la couche réseau (avant qu'elles atteignent le WAF)
  • Alerting — alerte sur les spikes de waf_event_rate
  • Custom Error Pages — brande la page 403 que le WAF retourne
  • Documentation OWASP CRS — référence des règles + taxonomie des tags
  • Documentation Coraza — internes du moteur WAF
  • internal/waf/category.go — mapping ID de règle Arenet → catégorie
  • internal/caddymgr/manager.go (lignes ~1400-2550) — emit du handler WAF + shape SecAction par route

Clone this wiki locally