fix(coraza): add deny rules that act on Coraza's verdict + spop-check on backend
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 55s
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 55s
Two fixes that complete the SPOE enforcement path:
1. Listener was sending requests to Coraza for inspection but never reading
the result. Coraza-SPOA sets var(txn.coraza.action) to "deny" / "drop"
/ "redirect" when a rule with that disruptive action fires; HAProxy
needs explicit rules that READ the variable and apply the action.
Without them, the audit log shows "Access denied" but the request
still gets HTTP 200 (verified on staging: sqlmap/JNDI/shellinj all
detected, all returned 200).
Added the standard six rules from upstream's example/haproxy/haproxy.cfg
covering http-request + http-response phases for each of deny/drop/
redirect. Same set the upstream Coraza-SPOA docs recommend.
Intentionally did NOT add the upstream's fail-CLOSED rule
`http-request deny deny_status 500 if { var(txn.coraza.error) -m int gt 0 }`
— for a hosting platform we want fail-open. Documented inline.
2. Backend health check switched from plain TCP `check` to `option
spop-check`. The spop-check actually negotiates a SPOE session against
the agent, so HAProxy detects a half-broken SPOA that's listening on
:9000 but failing protocol handshakes. Plain `check` would miss that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,11 @@
|
|||||||
# container's name + 9000 inside the shared docker network).
|
# container's name + 9000 inside the shared docker network).
|
||||||
backend coraza-spoa-backend
|
backend coraza-spoa-backend
|
||||||
mode tcp
|
mode tcp
|
||||||
|
# spop-check actually speaks the SPOE protocol against the agent —
|
||||||
|
# confirms the agent can negotiate a session, not just that the TCP
|
||||||
|
# port is open. Required to detect a half-broken SPOA that's listening
|
||||||
|
# but not actually processing.
|
||||||
|
option spop-check
|
||||||
timeout connect 5s
|
timeout connect 5s
|
||||||
timeout server 30s
|
timeout server 30s
|
||||||
# Keep-alive connection to the SPOA — saves a TCP handshake on every
|
server coraza-spoa {{ agent_target }} check
|
||||||
# request. SPOE protocol multiplexes multiple requests over one
|
|
||||||
# connection so this is normal.
|
|
||||||
server coraza-spoa {{ agent_target }} check inter 30s rise 2 fall 3
|
|
||||||
|
|||||||
@@ -58,9 +58,23 @@ frontend web
|
|||||||
# Coraza WAF inspection via SPOE. Runs AFTER rate-limit and IP-block
|
# Coraza WAF inspection via SPOE. Runs AFTER rate-limit and IP-block
|
||||||
# guards (no point asking the WAF about requests we're already dropping)
|
# guards (no point asking the WAF about requests we're already dropping)
|
||||||
# and AFTER the real-client-IP resolution (so Coraza sees the right src).
|
# and AFTER the real-client-IP resolution (so Coraza sees the right src).
|
||||||
# Fail-open: option set-on-error in coraza-spoe.cfg only SETS the error
|
|
||||||
# var; we deliberately don't have a `http-request deny if errored` rule,
|
|
||||||
# so SPOA outages let traffic through uninspected.
|
|
||||||
filter spoe engine coraza config /etc/haproxy/coraza-spoe.cfg
|
filter spoe engine coraza config /etc/haproxy/coraza-spoe.cfg
|
||||||
http-request send-spoe-group coraza coraza-req
|
http-request send-spoe-group coraza coraza-req
|
||||||
|
|
||||||
|
# Enforce Coraza's verdict. The SPOA sets var(txn.coraza.action) to
|
||||||
|
# "deny" / "drop" / "redirect" when a rule with the corresponding
|
||||||
|
# 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 }
|
||||||
|
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 }
|
||||||
|
http-response redirect code 302 location %[var(txn.coraza.data)] if { var(txn.coraza.action) -m str redirect }
|
||||||
|
|
||||||
|
# FAIL-OPEN on SPOA error. Upstream's example does the opposite — denies
|
||||||
|
# 500 if var(txn.coraza.error) is set — but for a hosting platform we'd
|
||||||
|
# rather lose WAF coverage briefly than 503 customer sites. The error
|
||||||
|
# variable still gets set, so monitoring can observe it.
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user