diff --git a/Dockerfile b/Dockerfile
index 2df21ea..36dd559 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -17,6 +17,11 @@ COPY haproxy_manager.py /haproxy/
COPY scripts /haproxy/scripts
COPY trusted_ips.list /etc/haproxy/trusted_ips.list
COPY trusted_ips.map /etc/haproxy/trusted_ips.map
+# /etc/haproxy is a named volume in deployed containers, so baked-in files
+# under that path get shadowed by the volume on existing deployments.
+# Place errorfiles outside the volumed path; the HAProxy config references
+# them by absolute path.
+COPY errors /haproxy/errors
RUN chmod +x /haproxy/scripts/*
RUN pip install -r requirements.txt
# Create log directories
diff --git a/errors/403-waf.html b/errors/403-waf.html
new file mode 100644
index 0000000..6bcace1
--- /dev/null
+++ b/errors/403-waf.html
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+Request blocked · %[req.hdr(host)]
+
+
+
+
+ Access blocked
+ Your request was blocked by our security filter
+ The request to %[req.hdr(host)] looked suspicious to our web application firewall and was not delivered to the site.
+ This is automated. No one has reviewed the request yet.
+
+
+ Request reference
+ %[unique-id]
+
+
+
+
Site owner?
+
If you operate this site and believe this block is incorrect, please open a support ticket and include the request reference above. Our team can look up exactly which rule fired and adjust it if it's a false positive.
+
+
+ Reference IDs expire from our active logs after 14 days, so please open a ticket promptly if you'd like this investigated.
+
+
+
diff --git a/templates/hap_header.tpl b/templates/hap_header.tpl
index 0a76d3f..9373f25 100644
--- a/templates/hap_header.tpl
+++ b/templates/hap_header.tpl
@@ -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
\ No newline at end of file
diff --git a/templates/hap_listener.tpl b/templates/hap_listener.tpl
index 0ce73b8..e828eb8 100644
--- a/templates/hap_listener.tpl
+++ b/templates/hap_listener.tpl
@@ -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 }