waf-block: render a real HTML page on Coraza-denied requests
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 1m55s
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 1m55s
Previously a Coraza block returned an empty 403 with only the `waf-block: request` header — a legitimate site owner caught in a false-positive had no idea what happened or how to get help. Now: - hap_header.tpl: every request gets a unique-id (uuid()) and that ID is injected back into the request as X-Request-Reference for the backend, so upstream Apache/PHP logs can correlate too. - hap_listener.tpl: on a request-phase Coraza deny we use `http-request return` with `lf-file` instead of `http-request deny`, so HAProxy renders the new errors/403-waf.html page with the request reference substituted in. The page tells the visitor a request was blocked, displays the reference, and points site owners to https://secure.anhonesthost.com/submitticket.php to open a ticket rather than exposing a public email address (avoids giving attackers a flood target). - The waf-block header and x-request-reference header are still set on the response so curl / monitoring clients can pick them up without rendering HTML. - Response-phase deny stays as the bare 403 — outbound blocks are rare in our config and an HTML body could land mid-stream. Errorfile lives at /haproxy/errors/403-waf.html (NOT under /etc/haproxy/, because that path is a named volume in deployed containers and would shadow baked-in files on existing deployments). Support workflow: visitor quotes the reference → support greps /var/log/haproxy.log for the uuid → gets timestamp + client IP + Host + URI → greps /var/log/coraza/audit.log for the matching transaction → reads the rule_id that fired. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,4 +73,15 @@ defaults
|
||||
timeout check 10s
|
||||
timeout tarpit 10s # Tarpit delay for low-level scanners (before silent-drop)
|
||||
maxconn 3000
|
||||
|
||||
# Per-request unique reference, used:
|
||||
# - in the log line (httplog includes %ID)
|
||||
# - echoed to clients in the X-Request-Reference response header on
|
||||
# WAF blocks so a customer can quote it when opening a support ticket
|
||||
# - embedded in /etc/haproxy/errors/403-waf.html so a blocked visitor
|
||||
# sees it on the rendered 403 page
|
||||
# Support correlates ref → /var/log/haproxy.log line → timestamp+client+host
|
||||
# → /var/log/coraza/audit.log entry → rule_id.
|
||||
unique-id-format %[uuid()]
|
||||
unique-id-header X-Request-Reference
|
||||
|
||||
@@ -85,8 +85,17 @@ frontend web
|
||||
# disruptive action fires (depends on SecRuleEngine mode + per-rule
|
||||
# ctl:ruleEngine overrides). Without these rules, Coraza would inspect
|
||||
# but never block.
|
||||
http-request deny deny_status 403 hdr waf-block "request" if { var(txn.coraza.action) -m str deny }
|
||||
http-response deny deny_status 403 hdr waf-block "response" if { var(txn.coraza.action) -m str deny }
|
||||
#
|
||||
# On request-phase deny we return a rendered HTML page that surfaces the
|
||||
# request reference (the unique-id) so a customer who's been blocked
|
||||
# incorrectly can open a support ticket and quote it. lf-file expands
|
||||
# log-format expressions inside the file at response time, so
|
||||
# %[unique-id] / %[req.hdr(host)] / etc. get substituted live.
|
||||
# Response-phase deny stays as a bare 403 — outbound blocks are rare in
|
||||
# our config (Coraza response inspection is disabled by default) and
|
||||
# an HTML body on a 403 generated mid-response could land mid-stream.
|
||||
http-request return status 403 content-type "text/html; charset=utf-8" hdr waf-block "request" hdr x-request-reference "%[unique-id]" lf-file /haproxy/errors/403-waf.html if { var(txn.coraza.action) -m str deny }
|
||||
http-response deny deny_status 403 hdr waf-block "response" hdr x-request-reference "%[unique-id]" if { var(txn.coraza.action) -m str deny }
|
||||
http-request silent-drop if { var(txn.coraza.action) -m str drop }
|
||||
http-response silent-drop if { var(txn.coraza.action) -m str drop }
|
||||
http-request redirect code 302 location %[var(txn.coraza.data)] if { var(txn.coraza.action) -m str redirect }
|
||||
|
||||
Reference in New Issue
Block a user