Skip to content

Commit 71f272f

Browse files
committed
fix(waf): fail closed when Coraza inspection fails
1 parent 2c36eef commit 71f272f

4 files changed

Lines changed: 57 additions & 13 deletions

File tree

configs/haproxy/README.md

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,24 @@ Coraza needs to run request-phase rules:
7878
Response-phase inspection is deliberately out of scope for M1
7979
(see ADR-007).
8080

81-
## Failure behaviour
81+
## Degraded-mode behaviour
8282

8383
`spoe-agent` is configured with `option set-on-error error`. If the
84-
SPOA is unreachable or returns an error, `txn.coraza.action` will not
85-
equal `"deny"` and the request is forwarded — i.e. the proxy
86-
fail-opens. Hardening this into an explicit degraded mode is tracked
87-
in #80; M1 only needs the happy path.
84+
SPOA is unreachable, times out, returns a malformed response, or
85+
returns an internal processing error, HAProxy sets
86+
`txn.coraza.error` to the SPOE/SPOP error code.
87+
88+
The M1 reference configuration fails closed for protected traffic:
89+
when `txn.coraza.error` is present, HAProxy returns
90+
`503 Service Unavailable` before contacting `be_app`. The response
91+
includes `X-WAF-Degraded: true` and `X-WAF-Error: <code>` so operators
92+
can distinguish WAF degraded mode from an application outage. HAProxy
93+
also raises the request log level to `err` for these requests.
94+
95+
This covers startup or unhealthy Coraza containers, connection
96+
failures, SPOE processing timeouts, malformed WAF responses, and
97+
transient runtime failures. Backend/dashboard status reporting is
98+
tracked separately in #69.
8899

89100
## Troubleshooting SPOE frames
90101

@@ -128,9 +139,10 @@ mode uses `info` logging.
128139
5. If HAProxy returns `421`, the request failed the reference host ACL
129140
before routing. Retry with `Host: app.local`.
130141

131-
6. If HAProxy returns an application response while Coraza is down,
132-
this is the expected M1 fail-open behavior from `option
133-
set-on-error error`; degraded-mode handling is tracked in #80.
142+
6. If HAProxy returns `503` with `X-WAF-Degraded: true`, Coraza/SPOA
143+
inspection failed and the proxy failed closed before contacting the
144+
backend. Use the `X-WAF-Error` value and HAProxy `err` log line to
145+
identify the SPOE/SPOP failure class.
134146

135147
For raw frame inspection in the Docker Compose setup, capture the SPOA
136148
traffic from inside the `haproxy` container while reproducing the

configs/haproxy/coraza.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ spoe-agent coraza-agent
1818
option var-prefix coraza
1919

2020
# If the SPOA is unreachable or replies with an error, mark the
21-
# transaction as errored. haproxy.cfg currently fail-opens in
22-
# that case; failure handling is tracked separately in #80.
21+
# transaction as errored. haproxy.cfg fails closed with 503 when
22+
# txn.coraza.error is present.
2323
option set-on-error error
2424

2525
# SPOE handshake / idle / per-request timeouts. Keep processing

configs/haproxy/haproxy.cfg

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,13 @@ frontend fe_http
4646
acl host_app hdr(host) -i app.local app.local:80 app.local:8080 localhost localhost:8080 127.0.0.1 127.0.0.1:8080
4747
http-request deny deny_status 421 if !host_app
4848

49-
# Coraza populates txn.coraza.* via the SPOE response. If the
50-
# engine returns action=deny we stop the transaction with 403.
49+
# Coraza populates txn.coraza.* via the SPOE response. If SPOE
50+
# inspection fails, fail closed before the backend is contacted.
51+
http-request set-log-level err if { var(txn.coraza.error) -m found }
52+
http-request return status 503 content-type text/plain string "WAF inspection unavailable" hdr X-WAF-Degraded true hdr X-WAF-Error %[var(txn.coraza.error)] if { var(txn.coraza.error) -m found }
53+
54+
# If the engine explicitly returns action=deny we stop the
55+
# transaction with 403.
5156
http-request deny deny_status 403 if { var(txn.coraza.action) -m str deny }
5257

5358
# Optional: log the WAF anomaly score on allowed requests so it

src/backend/tests/unit/test_waf_debug_reference_config.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ def test_debug_coraza_spoa_config_enables_debug_logging() -> None:
4444
def test_debug_compose_override_enables_haproxy_debug_flag() -> None:
4545
compose = (REPO_ROOT / "deploy/docker/docker-compose.debug.yml").read_text()
4646

47-
assert 'command: ["haproxy", "-d", "-f", "/usr/local/etc/haproxy/haproxy.cfg"]' in compose
47+
assert (
48+
'command: ["haproxy", "-d", "-f", "/usr/local/etc/haproxy/haproxy.cfg"]'
49+
in compose
50+
)
4851

4952

5053
def test_debug_compose_override_mounts_debug_coraza_config() -> None:
@@ -68,3 +71,27 @@ def test_haproxy_readme_documents_spoe_troubleshooting() -> None:
6871
assert "make dev" in readme
6972
assert "X-Request-ID: spoe-debug-1" in readme
7073
assert "tcpdump -i any -A -s 0 port 9000" in readme
74+
75+
76+
def test_reference_haproxy_config_fails_closed_on_spoe_errors() -> None:
77+
config = (REPO_ROOT / "configs/haproxy/haproxy.cfg").read_text()
78+
79+
assert "var(txn.coraza.error) -m found" in config
80+
assert (
81+
"http-request set-log-level err if { var(txn.coraza.error) -m found }"
82+
in config
83+
)
84+
assert "http-request return status 503" in config
85+
assert "X-WAF-Degraded true" in config
86+
assert "X-WAF-Error %[var(txn.coraza.error)]" in config
87+
88+
89+
def test_haproxy_readme_documents_fail_closed_degraded_mode() -> None:
90+
readme = (REPO_ROOT / "configs/haproxy/README.md").read_text()
91+
92+
assert "## Degraded-mode behaviour" in readme
93+
assert "fails closed" in readme
94+
assert "503 Service Unavailable" in readme
95+
assert "X-WAF-Degraded: true" in readme
96+
assert "tracked separately in #69" in readme
97+
assert "fail-open" not in readme

0 commit comments

Comments
 (0)