From f1e9bb2c63e9e47a79828790408635d0f4647d2d Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Tue, 12 May 2026 17:12:09 -0700 Subject: [PATCH] fix(coraza-spoe): match upstream's required spoe shape (groups, arg order, names) Three real bugs in the SPOE config caught when HAProxy validated the generated file: 1. spoe-agent must declare `groups` not `messages`. The `messages` form doesn't make the message reachable via `send-spoe-group`; HAProxy complained: unable to find SPOE group 'coraza-check' into SPOE engine 'coraza' 2. send-spoe-group references a spoe-GROUP name, which needs its own block. Added `spoe-group coraza-req { messages coraza-req }` as the indirection layer. 3. Arg names + ORDER are required to match what Coraza-SPOA parses positionally. My version had `dest-ip`/`dest-port`; upstream's example/haproxy/coraza.cfg (v0.7.1) uses `dst-ip`/`dst-port`. Renamed and reordered to match upstream verbatim, including the `app=str(haproxy)` literal that matches our config.yaml application name. Also corrected misleading comment about `set-on-error continue`: that option actually sets a variable on error; the fail-open behavior comes from us deliberately NOT adding a `http-request deny if errored` rule in the frontend. Renamed the variable to `error` (matching upstream) and updated comments to be accurate. Listener template's send-spoe-group action updated to reference the new group name `coraza-req`. Co-Authored-By: Claude Opus 4.7 (1M context) --- templates/hap_coraza_spoe_engine.tpl | 52 +++++++++++++++++----------- templates/hap_listener.tpl | 6 ++-- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/templates/hap_coraza_spoe_engine.tpl b/templates/hap_coraza_spoe_engine.tpl index 37a64a3..3e55db5 100644 --- a/templates/hap_coraza_spoe_engine.tpl +++ b/templates/hap_coraza_spoe_engine.tpl @@ -5,25 +5,33 @@ # via `filter spoe engine coraza config /etc/haproxy/coraza-spoe.cfg`. # # Engine name "coraza" must match the engine name in the filter line in the -# main config and the application name "haproxy" must match the application -# block name in coraza-spoa's config.yaml. +# main config; group name "coraza-req" must match the send-spoe-group action. +# Application name "haproxy" must match the application block in coraza-spoa's +# config.yaml. +# +# Reference: this config follows the shape from coraza-spoa's upstream +# example/haproxy/coraza.cfg (v0.7.1). Arg names + ordering are required by +# Coraza-SPOA exactly as specified — DO NOT reorder or rename without +# coordinating with the agent. [coraza] spoe-agent coraza - # The single message we send (defined below) — per-request inspection. - messages coraza-check + # `groups` (not `messages`) lists the spoe-group names this engine offers + # via `send-spoe-group` actions. The same group name appears below in a + # spoe-group block, which in turn references the actual message. + groups coraza-req - # Prefix for any variables the agent sets back on the request. + # Prefix for variables the agent sets back on the request transaction — + # e.g. var(txn.coraza.error) when set-on-error triggers. option var-prefix coraza - # FAIL-OPEN. If the SPOA is unreachable or times out, requests flow - # through uninspected rather than failing. For a hosting platform, - # availability beats unconditional inspection coverage. - option set-on-error continue + # On agent error/timeout, set var(txn.coraza.error). We DON'T add a + # corresponding `http-request deny if { var(txn.coraza.error) -m bool }` + # in the frontend, so the request continues uninspected. This is the + # fail-open posture: WAF outage shouldn't 503 customer traffic. + option set-on-error error - # Aggressive timeouts: we don't want the WAF to materially slow page - # loads. processing 100ms is the per-request inspection budget. timeout hello 2s timeout idle 2m timeout processing 100ms @@ -31,12 +39,16 @@ spoe-agent coraza use-backend coraza-spoa-backend log global -spoe-message coraza-check - # Send the request shape to Coraza for inspection. - # `app=str(haproxy)` matches the application named "haproxy" in - # coraza-spoa's config.yaml — that's how Coraza picks which ruleset - # to apply. - # NOTE: args must be on ONE line. HAProxy does not support backslash - # line continuations in spoe configs (verified the hard way 2026-05-12). - args app=str(haproxy) src-ip=src src-port=src_port dest-ip=dst dest-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body - event on-frontend-http-request +# Per-request inspection message. No `event` directive — fires only when +# explicitly invoked from haproxy.cfg via `http-request send-spoe-group`. +# Arg order/names are mandatory: Coraza-SPOA parses positionally and renames +# break the agent. `app=str(haproxy)` is the literal application name from +# coraza-spoa's config.yaml `applications:` block. +spoe-message coraza-req + args app=str(haproxy) 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 + +# Group binding for send-spoe-group invocation in the frontend. One group, +# one message; could add more in the future (e.g. coraza-res for response +# inspection — currently disabled in coraza-spoa's config.yaml). +spoe-group coraza-req + messages coraza-req diff --git a/templates/hap_listener.tpl b/templates/hap_listener.tpl index a5d924f..f647600 100644 --- a/templates/hap_listener.tpl +++ b/templates/hap_listener.tpl @@ -58,7 +58,9 @@ frontend web # Coraza WAF inspection via SPOE. Runs AFTER rate-limit and IP-block # 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). - # Fail-open: see `option set-on-error continue` in /etc/haproxy/coraza-spoe.cfg. + # 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 - http-request send-spoe-group coraza coraza-check + http-request send-spoe-group coraza coraza-req {%- endif %}