WAF mit HAProxy und Coraza
HAProxy mit Coraza WAF
Übersicht
HAProxy terminiert das TLS, reicht den entschlüsselten Request an Coraza zur Inspektion weiter und leitet ihn bei sauberem Verdict an das Backend durch. Coraza läuft als eigenständiger SPOE-Agent (Go) und führt das OWASP Core Rule Set aus.
- Datenpfad
waf.it2XX.xinmen.de --> 172.26.52.43 (HAProxy + Coraza) --> 172.26.52.11 (VulnSite, geschützt) www.it2XX.xinmen.de --> 172.26.52.11 (VulnSite, direkt, ungeschützt)
- Hinweis
- Beide Namen zeigen auf dieselbe VulnSite. Über
waf.filtert Coraza, überwww.geht der Angriff ungehindert durch — das ist der Demo-Kontrast "mit/ohne WAF".
Voraussetzungen
Die DMZ-IPs (172.26.52.0/24) sind über alle Labs identisch, nur der externe 2XX-Teil variiert. Das Wildcard-Zertifikat für *.xinmen.de liegt fertig vor (kein Let's Encrypt).
HAProxy installieren
HAProxy kommt aus dem Debian-Repo und läuft als eigener Dienst. Die Version muss SPOE unterstützen (Debian 13 Trixie bringt HAProxy 3.x mit).
- apt update
- apt install -y haproxy
- haproxy -v
Wildcard-Zertifikat bereitstellen
HAProxy erwartet Zertifikat, Intermediate-Chain und Private Key in einer einzigen PEM-Datei. Reihenfolge: erst das Server-Zert, dann die Chain, zuletzt der Key.
- mkdir -p /etc/haproxy/ssl
- cat /etc/ssl/own.crt /etc/ssl/own.key > /etc/haproxy/ssl/own.pem
- chmod 600 /etc/haproxy/ssl/own.pem
- Hinweis
- Liegt der Key nicht mit drin, startet HAProxy nicht und meldet unable to load SSL private key. Die Datei ist sicherheitskritisch, daher
chmod 600.
HAProxy-Grundkonfiguration
Die folgende Konfiguration terminiert TLS auf der WAF-IP und reicht an die VulnSite weiter. Der Coraza-Filter wird weiter unten ergänzt — zunächst läuft der Proxy ohne WAF.
- nano /etc/haproxy/haproxy.cfg
global
log /dev/log local0
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
user haproxy
group haproxy
daemon
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
log global
mode http
option httplog
timeout connect 5s
timeout client 30s
timeout server 30s
frontend ft_https
bind 172.26.52.43:443 ssl crt /etc/haproxy/ssl/own.pem alpn http/1.1
mode http
acl host_waf hdr(host) -i waf
use_backend bk_vulnsite if host_waf
default_backend bk_vulnsite
frontend ft_http_redirect
bind 172.26.52.43:80
mode http
http-request redirect scheme https code 301
backend bk_vulnsite
mode http
option forwardfor
server vuln 172.26.52.11:80 check
- Hinweis zu
option forwardfor - Setzt den
X-Forwarded-For-Header, damit Backend und SIEM die echte Client-IP sehen statt der WAF-IP. Das passt zum NAT-freien Design mit erhaltenen Source-IPs.
- Hinweis zu
alpn http/1.1 - Zwingend, damit HAProxy eigene Fehlerseiten (403) ausliefert. Ohne diese Option verhindert HTTP/2 das Rendern der Errorfiles.
Erster Test ohne WAF
- haproxy -c -f /etc/haproxy/haproxy.cfg
- systemctl restart haproxy.service
- curl -vk https://waf.it2XX.xinmen.de/
Erwartet: die VulnSite antwortet normal (HTTP 200).
Coraza SPOA installieren
Coraza läuft als eigenständiger Prozess neben HAProxy. Das Binary wird direkt aus dem Quellcode gebaut — Go ist dafür erforderlich.
apt install -y golang-go git
git clone https://github.com/corazawaf/coraza-spoa.git /opt/coraza-spoa
cd /opt/coraza-spoa
go build -o /usr/local/bin/coraza-spoa .
cd /
- Hinweis
./...funktioniert hier nicht, da das Repo mehrere Packages enthält. Das Binary landet direkt unter/usr/local/bin/— kein separatercp-Schritt nötig.
Coraza-Konfiguration
Die Konfiguration erfolgt als YAML-Datei. Die OWASP-Regeln (@owasp_crs/) sind im Binary eingebaut — ein separater CRS-Clone ist nicht nötig.
- mkdir -p /etc/coraza-spoa
- mkdir -p /var/log/coraza
- chown haproxy:haproxy /var/log/coraza
- chmod 750 /var/log/coraza
- vi /etc/coraza-spoa/coraza-spoa.yaml
bind: 127.0.0.1:9000
log_level: info
log_file: /var/log/coraza/coraza-spoa.log
log_format: json
default_application: sample_app
applications:
- name: sample_app
directives: |
Include @coraza.conf-recommended
Include @crs-setup.conf.example
Include @owasp_crs/*.conf
SecRuleEngine On
SecRequestBodyAccess On
SecResponseBodyAccess On
SecAuditLog /var/log/coraza/audit.log
SecAuditLogType Serial
SecAuditEngine RelevantOnly
response_check: false
transaction_ttl_ms: 60000
log_level: info
log_file: /var/log/coraza/coraza-spoa.log
log_format: json
- Hinweis
chownundchmodsind zwingend — coraza-spoa läuft als Userhaproxyund benötigt Schreibrecht auf das Log-Verzeichnis.
SPOE-Agent als Systemd-Service
Coraza-SPOA wird als eigener Systemd-Service betrieben. Bei einem Absturz startet systemd den Prozess automatisch neu.
- vi /etc/systemd/system/coraza-spoa.service
[Unit]
Description=Coraza SPOA for HAProxy
After=network.target
[Service]
ExecStart=/usr/local/bin/coraza-spoa -config /etc/coraza-spoa/coraza-spoa.yaml
Restart=on-failure
User=haproxy
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now coraza-spoa
SPOE-Konfiguration für HAProxy
SPOE (Stream Processing Offload Engine) ist der HAProxy-Mechanismus, um Requests an externe Prozesse auszulagern. Diese Datei verbindet HAProxy mit dem laufenden Coraza-Prozess.
- vi /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
spoe-message coraza-req
args app=str(sample_app) id=unique-id 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
- Wichtig
- Der Section-Header
[coraza]muss exakt mit dem engine-Namen inhaproxy.cfgübereinstimmen. Die Leerzeile am Dateiende ist zwingend, sonst meldet HAProxy Missing LF on last line.
HAProxy-Konfiguration um Coraza erweitern
HAProxy übergibt jeden eingehenden Request via SPOE an Coraza. Liefert Coraza den Verdict deny, blockiert HAProxy den Request mit HTTP 403.
Im frontend ft_https ergänzen:
frontend ft_https
bind 172.26.52.43:443 ssl crt /etc/haproxy/ssl/wildcard-xinmen.pem alpn http/1.1
mode http
filter spoe engine coraza config /etc/haproxy/coraza.cfg # NEU
http-request deny deny_status 403 if { var(txn.coraza.action) -m str deny } # NEU
acl host_waf hdr(host) -i waf.it2XX.xinmen.de
use_backend bk_vulnsite if host_waf
default_backend bk_vulnsite
Backend für den Coraza-SPOA hinzufügen:
backend coraza-spoa # NEU
mode tcp # NEU
server coraza 127.0.0.1:9000 # NEU
- Wichtig
mode tcpim SPOA-Backend ist zwingend — ohne das schlägt der Config-Check fehl. Die VulnSite läuft weiter inmode http, nur der SPOE-Kanal zu Coraza ist TCP.
Konfiguration testen und neu laden
- haproxy -c -f /etc/haproxy/haproxy.cfg
- systemctl reload haproxy
Test
Klassischer WAF-Test mit einem OWASP-typischen Angriff (SQL Injection) gegen den geschützten Pfad:
- curl -vk "https://waf.it2XX.xinmen.de/?id=1'+OR+'1'='1"
Erwartet: HTTP 403
XSS-Test:
- curl -vk "https://waf.it2XX.xinmen.de/?q=<script>alert(1)</script>"
Erwartet: HTTP 403
Derselbe Angriff gegen den ungeschützten Direktpfad:
- curl -vk "https://www.it2XX.xinmen.de/?id=1'+OR+'1'='1"
Erwartet: HTTP 200 — die VulnSite verarbeitet den Angriff ungehindert.
- Hinweis
- Genau dieser Vergleich ist das didaktische Kernstück. Derselbe Request, einmal von Coraza geblockt, einmal durchgelassen.
Eigene WAF-Sperrseite
HAProxy liefert bei einem von Coraza geblockten Request eine eigene Fehlerseite statt des nackten 403 aus. Die Datei muss eine vollständige HTTP-Antwort enthalten — inklusive Statuszeile und Headern.
- vi /etc/haproxy/errors/403-waf.http
HTTP/1.1 403 Forbidden
Cache-Control: no-cache
Connection: close
Content-Type: text/html; charset=utf-8
<!DOCTYPE html>
<html lang="de">
<head><meta charset="utf-8"><title>403 - Blockiert</title></head>
<body style="font-family:sans-serif;background:#0a0a0a;color:#e0e0e0;text-align:center;padding-top:80px">
<h1 style="color:#ff4444">403 - Request blockiert</h1>
<p>Diese Anfrage wurde von der Web Application Firewall (Coraza) abgewiesen.</p>
<p style="color:#888">OWASP Core Rule Set · xinmen.de WAF</p>
</body>
</html>
Im Frontend die Block-Regel um errorfile ergänzen:
frontend ft_https
bind 172.26.52.43:443 ssl crt /etc/haproxy/ssl/own.pem alpn http/1.1
mode http
filter spoe engine coraza config /etc/haproxy/coraza.cfg
http-request deny deny_status 403 if { var(txn.coraza.action) -m str deny }
errorfile 403 /etc/haproxy/errors/403-waf.http # NEU
acl host_waf hdr(host) -i waf.it2XX.xinmen.de
use_backend bk_vulnsite if host_waf
default_backend bk_vulnsite
- Wichtig
- Die Datei muss mit der vollständigen Statuszeile
HTTP/1.1 403 Forbiddenbeginnen und nach den Headern eine Leerzeile vor dem HTML-Body haben. Diealpn http/1.1-Option in der bind-Zeile ist Voraussetzung, sonst rendert HTTP/2 die Errorfile nicht.
Logs beobachten
- journalctl -fu coraza-spoa
- tail -f /var/log/coraza/audit.log
- journalctl -fu haproxy