WAF mit HAProxy und Coraza

Aus Xinux Wiki
Zur Navigation springen Zur Suche springen

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, über www. 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

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 separater cp-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
chown und chmod sind zwingend — coraza-spoa läuft als User haproxy und 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 in haproxy.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 tcp im SPOA-Backend ist zwingend — ohne das schlägt der Config-Check fehl. Die VulnSite läuft weiter in mode 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:

Erwartet: HTTP 403

XSS-Test:

Erwartet: HTTP 403

Derselbe Angriff gegen den ungeschützten Direktpfad:

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 &middot; 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 Forbidden beginnen und nach den Headern eine Leerzeile vor dem HTML-Body haben. Die alpn 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