Coraza-SPOA et OWASP CRS sur HAProxy
Déploiement de Coraza-SPOA et OWASP CRS avec HAProxy, HTTP/3, Fail2ban et les paquets HAProxy AWS-LC.
Dépôts
- Dépôt public disponible
Pourquoi j’ai construit cela
HAProxy reste mon proxy de périphérie préféré. Il est rapide, stable et se comporte de manière prévisible.
Je voulais une véritable protection WAF, mais aussi comprendre comment un WAF fonctionne réellement en interne. Pas simplement « l’activer et espérer que tout ira bien ». Je ne voulais ni d’une pile lourde tout-en-un, ni d’un système qui cache la logique derrière trop de couches.
Cette configuration oblige à comprendre le flux : comment les requêtes traversent HAProxy, comment Coraza évalue les règles et comment les décisions sont appliquées. Ce n’est pas prêt à l’emploi, mais c’est en partie le but. On voit ce qui se passe et on garde le contrôle.
J’ai donc construit la pile autour de :
- HAProxy en périphérie
- Coraza-SPOA comme moteur WAF
J’ai aussi étudié CrowdSec et BunkerWeb, mais cette combinaison m’a donné une meilleure visibilité sur ce qui était bloqué et pourquoi, et il était plus facile de la régler lorsqu’un ajustement s’imposait.
Mise à jour de mai 2026 : paquets HAProxy AWS-LC officiels
Le grand changement depuis la première version est que je ne compile plus HAProxy et AWS-LC depuis les sources par défaut.
HAProxy fournit maintenant des paquets officiels Community Performance compilés avec AWS-LC :
- Téléchargements officiels : https://www.haproxy.com/downloads
- Annonce de sortie : https://www.haproxy.com/company/news/haproxy-technologies-announces-availability-of-haproxy-community-performance-packages-compiled-with-aws-lc
Cela change la décision opérationnelle.
En janvier 2025, la compilation depuis les sources avait du sens pour mon nœud de périphérie Oracle ARM, car les paquets n’étaient pas disponibles dans mon environnement. Je voulais HAProxy 3.3, QUIC et AWS-LC, et compiler AWS-LC et HAProxy était la façon fiable d’y parvenir.
En mai 2026, ce n’est plus le choix par défaut. Le paquet haproxy-awslc me donne le même résultat pratique :
- HAProxy 3.3
- environnement d’exécution TLS AWS-LC
- prise en charge de QUIC / HTTP/3
- gestion par les paquets de la distribution
- procédure normale de mise à niveau et de retour arrière
Version actuelle dans mon déploiement de test Oracle ARM :
- HAProxy
3.3.10-0+ha33+ubuntu24.04u1 - Bibliothèque SSL en cours d’exécution :
AWS-LC 3.3.0 - Les options de compilation comprennent
USE_OPENSSL_AWSLC=1etUSE_QUIC=1 - Coraza-SPOA
v0.7.2 - OWASP CRS
v4.26.0
La compilation depuis les sources était utile, mais plus difficile à exploiter :
- Je devais gérer
/usr/local/src/aws-lc,/opt/aws-lc, les arborescences sources de HAProxy et les marqueurs d’état de compilation. - Les mises à niveau étaient manuelles.
La nouvelle règle est donc simple :
- Utiliser
haproxy-awslcdepuis le dépôt HAProxy performance lorsque le paquet existe pour la distribution cible et son architecture. - Ne garder la compilation depuis les sources qu’en solution de repli pour les plateformes non prises en charge ou les expériences en laboratoire.
- Nettoyer les anciens artefacts de compilation, sans supprimer les bibliothèques d’exécution fournies par les paquets.
Petit rappel :
- AWS-LC est la bibliothèque cryptographique d’Amazon. Elle offre une API compatible avec OpenSSL et de bonnes performances TLS.
- QUIC est le transport basé sur UDP utilisé par HTTP/3.
Pour approfondir les choix de piles TLS et leurs compromis, je recommande vivement cet article de HAProxy : https://www.haproxy.com/blog/state-of-ssl-stacks
J’aime toujours comprendre toute la chaîne : choix de la bibliothèque TLS, comportement de HTTP/3, journalisation, décisions du WAF et application des bannissements. Je ne veux simplement plus que le nœud de périphérie de production dépende d’une chaîne de compilation personnalisée alors qu’un paquet officiel existe.
1. Installer HAProxy 3.3 avec AWS-LC et QUIC
Sur Ubuntu 24.04 ARM, j’utilise maintenant le dépôt HAProxy performance et j’installe haproxy-awslc.
1
2
3
4
5
6
7
8
9
10
11
sudo apt-get install -y ca-certificates gnupg socat wget
sudo install -d -m 0755 /usr/share/keyrings
sudo wget -O /usr/share/keyrings/HAPROXY-key-community.asc \
https://pks.haproxy.com/linux/community/RPM-GPG-KEY-HAProxy
echo "deb [arch=arm64 signed-by=/usr/share/keyrings/HAPROXY-key-community.asc] https://www.haproxy.com/download/haproxy/performance/ubuntu/ha33 noble main" \
| sudo tee /etc/apt/sources.list.d/haproxy.list
sudo apt-get update
sudo apt-get install -y haproxy-awslc
haproxy -vv | grep -Ei 'HAProxy version|OPENSSL_AWSLC|QUIC|AWS-LC'
Sur mon nœud de périphérie Oracle ARM de test, cela indique :
1
2
3
4
HAProxy version 3.3.10-0+ha33+ubuntu24.04u1
OPTIONS = USE_OPENSSL=1 USE_OPENSSL_AWSLC=1 ... USE_QUIC=1 ...
Built with SSL library version : OpenSSL 1.1.1 (compatible; AWS-LC 3.3.0)
Running on SSL library version : AWS-LC 3.3.0
Si HAProxy est lancé par systemd sous l’utilisateur haproxy, QUIC sur UDP/443 exige toujours que le binaire conserve la capacité de liaison privilégiée :
1
2
3
sudo apt-get install -y libcap2-bin
sudo setcap cap_net_bind_service=+ep /usr/sbin/haproxy
sudo getcap /usr/sbin/haproxy
Sans cela, HAProxy peut démarrer, mais journaliser ceci après un redémarrage :
1
Permission error on QUIC socket binding for proxy front_webservers.
Veillez également à régler nbthread sur le nombre réel de processeurs. Mon nœud Oracle de périphérie en production a actuellement un seul processeur ; la section globale de HAProxy utilise donc :
nbthread 1
Si nbthread est trop élevé, HAProxy avertit que plusieurs threads sont liés au même processeur, ce qui peut dégrader les performances.
Le rôle Ansible supprime également les anciens chemins de compilation des sources, comme /usr/local/src/haproxy-*, ainsi que les anciens marqueurs d’état de compilation. Il refuse volontairement de nettoyer /opt/aws-lc, car l’environnement d’exécution AWS-LC de HAProxy installé par paquet utilise ce chemin.
2. Installer Coraza-SPOA
Coraza-SPOA est écrit en Go. J’installe Go, compile le binaire et l’exécute sous un compte système dédié.
Installer Go (choisir une version stable récente sur go.dev/dl/ et la bonne architecture pour l’hôte) :
1
2
3
4
5
6
wget https://go.dev/dl/go1.23.5.linux-arm64.tar.gz
sudo tar -C /usr/local -xzf go1.23.5.linux-arm64.tar.gz
export PATH=$PATH:/usr/local/go/bin
echo 'export GOPATH=$HOME/go' >> ~/.bashrc
source ~/.bashrc
go version
Compiler et installer Coraza-SPOA :
1
2
3
4
5
6
7
sudo apt install git make gcc pkg-config wget unzip
git clone https://github.com/corazawaf/coraza-spoa.git
cd ./coraza-spoa
go run mage.go build
addgroup --quiet --system coraza-spoa
adduser --quiet --system --ingroup coraza-spoa --no-create-home --home /nonexistent --disabled-password coraza-spoa
Créer les répertoires, récupérer CRS et ajuster la configuration :
1
2
3
4
5
6
7
8
9
10
11
12
13
mkdir -p /etc/coraza-spoa
cd /etc/coraza-spoa
sudo wget https://github.com/coreruleset/coreruleset/archive/refs/tags/v4.25.0.zip
mkdir -p /var/log/coraza-spoa /var/log/coraza-spoa/audit
touch /var/log/coraza-spoa/server.log /var/log/coraza-spoa/error.log /var/log/coraza-spoa/audit.log /var/log/coraza-spoa/debug.log
cp -a ./build/coraza-spoa /usr/bin/coraza-spoa
chmod 750 /usr/bin/coraza-spoa
cp -a ./example/coraza-spoa.yaml /etc/coraza-spoa/config.yaml
sed -i 's/bind: 0.0.0.0:9000/bind: 127.0.0.1:9000/' /etc/coraza-spoa/config.yaml
sed -i 's|log_file:.*|log_file: /var/log/coraza-spoa/coraza-agent.log|' /etc/coraza-spoa/config.yaml
3. Fonctionnement de Coraza avec HAProxy
Petit rappel :
- SPOE : Stream Processing Offload Engine (côté HAProxy)
- SPOA : Stream Processing Offload Agent (côté Coraza)
- SPOP : protocole entre les deux
Flux d’une requête :
- La requête atteint HAProxy.
- HAProxy envoie le contexte de la requête à Coraza via SPOE/SPOP.
- Coraza évalue les règles (CRS + règles du plugin + exclusions personnalisées).
- Coraza renvoie une décision (autoriser / refuser / rediriger / abandonner).
- HAProxy applique la décision.
Point important : Coraza ne remplace pas HAProxy. HAProxy continue de contrôler le trafic. Coraza est le moteur de décision du WAF.
Principaux fichiers de configuration
/etc/haproxy/haproxy.cfg/etc/haproxy/coraza.cfg/etc/coraza-spoa/config.yaml/etc/coraza-spoa/coraza.conf
4. Petit guide des règles CRS
Coraza utilise une syntaxe de type ModSecurity :
1
SecRule VARIABLES "OPERATOR" "TRANSFORMATIONS,ACTIONS"
Exemple :
1
SecRule REQUEST_URI "@streq /index.php" "id:1,phase:1,t:lowercase,deny"
Les phases sont :
- En-têtes de la requête
- Corps de la requête
- En-têtes de la réponse
- Corps de la réponse
- Journalisation
Référence : Documentation sur les phases de traitement de ModSecurity
5. Plugins CRS (exemple Nextcloud)
Pour Nextcloud, j’utilise le plugin Nextcloud officiel d’exclusions OWASP CRS.
Je garde aussi des exclusions ciblées par chemin lorsqu’elles sont nécessaires. Pas de contournement général.
6. Difficultés liées à QUIC
6.1 Effets secondaires de la compilation depuis les sources
Avec HAProxy compilé depuis les sources, les valeurs par défaut de la distribution peuvent manquer :
- utilisateur/groupe
haproxy - unité systemd
/etc/haproxy/errors/*.http- capacités Linux pour lier QUIC sur UDP/443 avec un processus de travail non privilégié
On peut donc avoir un bon binaire et tout de même échouer au démarrage ou à la validation.
6.2 Qualité de la journalisation
Si les en-têtes transférés et la gestion des adresses IP réelles dans Coraza ne sont pas alignés, les journaux peuvent afficher l’adresse de bouclage au lieu de l’adresse IP réelle du client. Cela complique beaucoup la réponse aux incidents.
6.3 Bannir correctement les clients QUIC abusifs
Cette partie a été plus délicate que prévu.
Au début, les bannissements semblaient actifs dans Fail2ban, mais le trafic HTTP/3 abusif continuait d’apparaître. Ce n’est pas simplement « TCP mauvais / UDP bon ».
Pour que les bannissements fonctionnent de manière fiable, plusieurs conditions doivent être réunies en même temps :
- HAProxy doit journaliser suffisamment de contexte pour établir une corrélation (adresse IP du client, code d’état, chemin de la requête, durée).
- Les décisions de Coraza doivent être clairement et systématiquement consignées dans les journaux.
- Les filtres Fail2ban doivent correspondre au format réel des journaux (journald ou fichiers, expression régulière correcte).
- L’action de bannissement doit bloquer tous les transports utilisés par les clients (TCP et UDP sur le port 443).
- Les règles nftables doivent être vérifiées dans les chaînes ou ensembles actifs après le rechargement de Fail2ban.
Si un seul maillon est incorrect, la protection semble activée, mais reste incomplète.
Modèle d’application
Attaquant (HTTP/3 sur UDP/443)
→ frontal HAProxy
→ refus Coraza / 403
→ Fail2ban détecte les événements répétés
→ nftables bannit la source pour TCP et UDP
Deux points importants :
- Fail2ban n’inspecte pas directement les paquets QUIC ; il réagit aux événements des journaux.
- Si la journalisation est incomplète ou retardée, l’application du bannissement le sera également.
Configuration Fail2ban qui a fonctionné
1
2
3
4
5
6
7
[coraza]
action = nftables[type=allports, protocol=tcp, name=coraza-tcp]
nftables[type=allports, protocol=udp, name=coraza-udp]
[haproxy-rate]
action = nftables[type=allports, protocol=tcp, name=haproxy-rate-tcp]
nftables[type=allports, protocol=udp, name=haproxy-rate-udp]
Vérifier ensuite trois choses simples :
- Vérifier que les jails sont bien chargés et actifs :
- fail2ban-client status
- fail2ban-client status coraza
- fail2ban-client status haproxy-rate
Vérifier que nftables contient réellement les règles de bannissement et qu’elles s’appliquent à TCP comme à UDP.
- Faire un vrai test depuis une adresse IP bannie : essayer d’atteindre le serveur en HTTP/2 et HTTP/3 et confirmer que l’accès est bloqué.
Il faut aussi rester réaliste avec QUIC. Comme il réutilise les connexions et réessaie différemment de TCP, un client peut sembler « actif » pendant un court instant juste après son bannissement. Les nouvelles connexions devraient être abandonnées, mais les connexions existantes peuvent persister brièvement.
7. Faux positifs et stratégie d’exclusion
Voici ce qui est resté stable pour moi :
- Garder CRS activé.
- Ajouter le plugin officiel de l’application lorsqu’il existe.
- Ajouter de petites exclusions ciblées uniquement pour les faux positifs avérés.
- Continuer à bloquer les sondes des scanners (
/.ssh/id_rsa, sondes génériques, etc.).
Pour DAV Nextcloud, des faux positifs liés à l’analyse syntaxique peuvent se produire. Il faut les traiter avec des exclusions ciblées par URI et méthode.
8. Note sur l’automatisation
Tout est déployé avec des rôles Ansible (WireGuard, Fail2ban, Coraza, ACME, HAProxy et vérifications post-déploiement) afin que la configuration reste reproductible. Tout refaire à la main serait pénible et propice aux erreurs.
Le déploiement privé cible maintenant les hôtes de périphérie Oracle :
prod-edge-oci-01test-edge-oci-01
Le rôle HAProxy conserve également les correctifs opérationnels :
haproxy_global_nbthread: 1pour la VM de production actuelle à 1 processeurcap_net_bind_service=+epsur/usr/sbin/haproxylorsque QUIC est activéhaproxy-awslcdepuis le dépôt HAProxy performance officiel- nettoyage des anciens répertoires de compilation des sources sans supprimer l’environnement d’exécution AWS-LC fourni par paquet
Les redéploiements sont ainsi bien plus propres. L’ancienne méthode de compilation depuis les sources passait plusieurs minutes à compiler AWS-LC et HAProxy et laissait davantage d’éléments à gérer. Avec les paquets, le rôle de périphérie converge via les opérations APT habituelles et peut se concentrer sur la validation de la configuration, les certificats, Coraza, Fail2ban et l’état des services.
9. Vérification à l’exécution
Voici les vérifications que j’effectue après un déploiement ou une reconstruction :
1
2
3
4
5
systemctl status coraza-spoa haproxy fail2ban
ss -ltnp | egrep '(:80|:443|:9000)'
ss -lunp | egrep ':443'
haproxy -vv | egrep 'HAProxy version|OPENSSL_AWSLC|QUIC|AWS-LC'
fail2ban-client status coraza
Pour un test WAF local rapide :
1
curl -H 'Host: xyz.domain.com' -D - http://127.0.0.1/.env
Résultat attendu :
1
2
HTTP/1.1 403 Forbidden
waf-block: request
Cela confirme que la requête atteint HAProxy, passe par Coraza via SPOE et est refusée par le chemin WAF.
10. Réflexions finales
La difficulté n’est plus de compiler HAProxy avec la bonne bibliothèque TLS sur ARM. Le paquet performance AWS-LC officiel a résolu ce problème pour ma configuration.
Le travail le plus difficile concerne maintenant l’exploitation :
- maintenir HAProxy, Coraza, ACME, Fail2ban et le pare-feu de l’hôte en cohérence ;
- vérifier le comportement de HTTP/2 et HTTP/3 ;
- veiller à ce que les décisions du WAF soient assez clairement journalisées pour permettre les bannissements ;
- éviter de cacher l’état de production dans des compilations ponctuelles depuis les sources.
C’est une meilleure façon d’employer son temps que d’entretenir une chaîne de compilation HAProxy personnalisée à chaque reconstruction du nœud de périphérie.
