WAF mit HAProxy und Coraza Rocky
WAF-Demo: HAProxy mit Coraza auf Rocky Linux
Ziel
Diese Übung zeigt eine minimale Web Application Firewall auf Basis von HAProxy und Coraza (OWASP CRS) vor einem einfachen Webserver. Gleichzeitig demonstriert der Aufbau, dass Rocky Linux bereits von Haus aus durch firewalld und SELinux gehärtet ist, bevor überhaupt eine WAF ins Spiel kommt. Bewusst nur eine einzige Komponente pro Aufgabe, ein Backend, eine Fehlerseite.
Topologie
- Beschreibung
Ein einzelner Rocky-Linux-Host waf01.it2XX.int übernimmt alle Rollen.
- HAProxy lauscht extern auf Port 80
- coraza-spoa (WAF-Engine) lauscht nur auf 127.0.0.1:9000
- httpd (Backend) lauscht nur auf 127.0.0.1:8888
Nur Port 80 (und SSH für die Administration) ist von außen erreichbar. Die interne Kommunikation zwischen HAProxy, Coraza und dem Backend bleibt vollständig auf Loopback.
Backend-Webserver vorbereiten
- httpd installieren
- dnf install -y httpd
- Backend auf einen unüblichen Port legen
Wir nutzen bewusst Port 8888 statt 80, um später mit SELinux arbeiten zu müssen statt uns auf den Standard-Kontext zu verlassen.
# /etc/httpd/conf.d/backend.conf
Listen 127.0.0.1:8888
<VirtualHost 127.0.0.1:8888>
DocumentRoot /var/www/backend
</VirtualHost>
- Testseite anlegen
- mkdir -p /var/www/backend
- echo "Backend erreichbar" > /var/www/backend/index.html
Härtung mit firewalld
- Beschreibung
Von außen darf ausschließlich HTTP (Port 80) und SSH erreichbar sein. Die internen Ports 8888 und 9000 hängen am Loopback und benötigen daher gar keine Freigabe.
- firewall-cmd --permanent --add-service=http
- firewall-cmd --permanent --add-service=ssh
- firewall-cmd --reload
- Kontrolle
- firewall-cmd --list-all
Härtung mit SELinux
- Beschreibung
httpd läuft standardmäßig unter dem Typ httpd_t. Port 8888 gehört aber nicht zur Standardliste von http_port_t, daher blockiert SELinux den Start zunächst, obwohl firewalld gar nicht betroffen ist – ein guter Anlass, den Unterschied zwischen Netzwerkfilter und Mandatory Access Control zu zeigen.
- Prüfen, welche Ports http_port_t bereits enthält
- semanage port -l | grep http_port_t
- Port 8888 korrekt zu http_port_t hinzufügen
- semanage port -a -t http_port_t -p tcp 8888
Damit kann sowohl httpd auf 8888 binden als auch HAProxy (Typ haproxy_t) sich dorthin verbinden, ohne die pauschale Boolean haproxy_connect_any aktivieren zu müssen. Bewusst kein chcon, sondern die persistente Policy-Änderung über semanage.
- httpd starten und aktivieren
- systemctl enable --now httpd
HAProxy installieren
- dnf install -y haproxy
Coraza (coraza-spoa) installieren
- Beschreibung
coraza-spoa ist ein eigenständiger Go-Daemon, der als SPOE-Agent für HAProxy arbeitet. Es gibt kein Rocky-Paket, aber ein fertiges Release-Binary, sodass kein Compiler auf dem System nötig ist.
- mkdir -p /etc/coraza-spoa
- curl -L -o /usr/local/bin/coraza-spoa https://github.com/corazawaf/coraza-spoa/releases/latest/download/coraza-spoa_linux_amd64
- chmod +x /usr/local/bin/coraza-spoa
- OWASP Core Rule Set und Basisregeln laden
- git clone --depth=1 https://github.com/coreruleset/coreruleset /etc/coraza-spoa/coreruleset
- curl -L -o /etc/coraza-spoa/coraza.conf https://raw.githubusercontent.com/corazawaf/coraza/main/coraza.conf-recommended
- cp /etc/coraza-spoa/coreruleset/crs-setup.conf.example /etc/coraza-spoa/crs-setup.conf
- Coraza-Konfiguration
# /etc/coraza-spoa/config.yaml
bind: 127.0.0.1:9000
loglevel: info
applications:
waf01:
directives: |
Include /etc/coraza-spoa/coraza.conf
SecRuleEngine On
Include /etc/coraza-spoa/crs-setup.conf
Include /etc/coraza-spoa/coreruleset/rules/*.conf
- systemd-Unit
# /etc/systemd/system/coraza-spoa.service [Unit] Description=Coraza SPOA für HAProxy After=network.target [Service] ExecStart=/usr/local/bin/coraza-spoa -config /etc/coraza-spoa/config.yaml Restart=on-failure [Install] WantedBy=multi-user.target
- systemctl daemon-reload
- systemctl enable --now coraza-spoa
HAProxy-Konfiguration
- SPOE-Filter einbinden und Backend definieren
# /etc/haproxy/haproxy.cfg
global
log /dev/log local0
maxconn 2000
defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
frontend fe_web
bind *:80
filter spoe engine coraza config /etc/haproxy/coraza.cfg
http-request deny deny_status 403 if { var(txn.coraza.action) -m str deny }
default_backend backend_web
backend backend_web
server web01 127.0.0.1:8888 check
backend coraza-spoa
mode tcp
option spop-check
server spoa1 127.0.0.1:9000 check
SPOE-Konfiguration für Coraza
# /etc/haproxy/coraza.cfg
[coraza]
spoe-agent coraza-agent
messages coraza-req
option var-prefix coraza
option set-on-error error
timeout hello 2s
timeout idle 2m
timeout processing 500ms
use-backend coraza-spoa
log global
spoe-message coraza-req
args app=str(waf01) src-ip=src src-port=src_port dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body
event on-frontend-http-request
Eigene Fehlerseite
- Beschreibung
Genau eine Fehlerseite für alle von der WAF geblockten Requests, kein zusätzliches Fehlerseiten-Set.
# /etc/haproxy/errors/403-waf.http HTTP/1.1 403 Forbidden Content-Type: text/html <html><body><h1>Anfrage blockiert</h1><p>Diese Anfrage wurde von der Web Application Firewall abgelehnt.</p></body></html>
- Fehlerseite in haproxy.cfg einbinden
In den defaults-Block ergänzen:
errorfile 403 /etc/haproxy/errors/403-waf.http
- systemctl enable --now haproxy
Test
- Normale Anfrage
- curl -i http://waf01.it2XX.int/
- Angriffsversuch, der von Coraza/OWASP CRS erkannt werden sollte
- curl -i "http://waf01.it2XX.int/?id=1' OR '1'='1"
Die erste Anfrage liefert die Backend-Testseite mit Status 200, die zweite die eigene 403-Fehlerseite, ohne dass die Anfrage überhaupt das Backend erreicht.
Zusammenfassung
- firewalld lässt von außen ausschließlich HTTP und SSH zu
- SELinux verhindert, dass httpd oder HAProxy Ports außerhalb der freigegebenen Policy nutzen, unabhängig von firewalld
- HAProxy mit Coraza blockiert bekannte Angriffsmuster, bevor sie das Backend erreichen
- Genau eine Fehlerseite für alle WAF-Blocks