2 Commits

Author SHA1 Message Date
ba4c101135 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
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>
2026-05-12 17:16:03 -07:00
73b9104565 PR 2/3: opt-in SPOE integration for Coraza WAF
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 1m59s
Adds the plumbing that lets haproxy-manager talk to the coraza-spoa sidecar
added in PR 1, while keeping the default behavior bit-identical for any
deployment that doesn't set the new env var (the home network / standalone
use cases).

Single gate: HAPROXY_CORAZA_SPOE_BACKEND env var on the haproxy-manager
container. Unset (default) = generate_config() renders zero SPOE-related
output. Set (e.g. "coraza-spoa:9000") = three things happen at config
generation time:

  1. hap_listener.tpl injects 5 lines at the end of the frontend block:
       filter spoe engine coraza config /etc/haproxy/coraza-spoe.cfg
       http-request send-spoe-group coraza coraza-check
     ...placed AFTER rate-limit and IP-block guards so we don't waste WAF
     calls on requests we were going to drop anyway.

  2. A new TCP backend (hap_coraza_spoa_backend.tpl) is appended:
       backend coraza-spoa-backend
           mode tcp
           server coraza-spoa <env-var-target> check ...

  3. The SPOE engine config (hap_coraza_spoe_engine.tpl) is rendered and
     written to /etc/haproxy/coraza-spoe.cfg, defining the spoe-agent
     "coraza" + spoe-message "coraza-check". This sets:
       - option set-on-error continue   (FAIL-OPEN if SPOA is unreachable)
       - timeout processing 100ms       (per-request inspection budget)
       - app=str(haproxy)               (matches sidecar's application name)

Verification (template render only, before staging deploy):
  - hap_listener.tpl with no env var: 55 lines, zero SPOE references
  - hap_listener.tpl with env var:    62 lines, filter + send-spoe-group present
  - Engine cfg + backend block render with correct agent_target substitution

Next: PR 3 wires this into WHP (sidecar deploy via container-manager.sh
extension, server-settings UI for on/off, AI Monitor source for the audit
log). Staging verification of PR 1 + PR 2 together happens after PR 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:49:29 -07:00