Skip to content

fix(edge): coraza-caddy 2.6.1 so streaming responses survive the WAF - #17

Merged
marcoome merged 1 commit into
mainfrom
fix/edge-coraza-caddy-2.6.1
Sep 16, 2026
Merged

marcoome merged 1 commit into
mainfrom
fix/edge-coraza-caddy-2.6.1

Conversation

@marcoome

Copy link
Copy Markdown
Contributor

Refs #14 - second, separate bug behind the report. #15 fixed the blocking-mode false positive; this one is why the reporter's workaround and detection-only mode still did not help.

Root cause

coraza-caddy 2.5.0 (edge image up to v1.4.8) propagates Flush() with a bare http.Flusher type assertion. Caddy wraps the response writer in a responseRecorder whenever access logging is on, and the panel enables access logs on every node's srv0. That recorder exposes flushing only through Unwrap(), so every flush was silently dropped. Any streaming response behind a WAF-enabled host stalled until the server buffer filled or the handler returned: an EventSource never received its headers. Blocking vs detection-only made no difference; only disabling the WAF removed the wrapper.

My earlier repro used BuildRoute output inside a bare server config without logs, which is why it did not show up. Lesson recorded: reproduce node issues with the full generated server config.

Fix

Bump the edge image pin to coraza-caddy 2.6.1 - upstream fix corazawaf/coraza-caddy#344 "flush and hijack through the response writer Unwrap chain". coraza v3.7.0 and CRS 4.25.0 are unchanged, so the audit log the node-agent parses is unaffected. 2.6.1 needs Go 1.26; caddy:2.11.4-builder ships golang:1.26-alpine.

Verification (caddy 2.11.4, real generated route, srv0.logs on)

coraza-caddy curl SSE Beszel in headless Chrome
2.5.0 0 events, no headers GET /api/realtime pending forever
2.6.1 4 events, ttfb 3 ms GET 200, subscriptions POST 204
2.6.1, blocking - POST 403 (CRS 942100, covered by #15)
2.6.1, blocking + SecRuleRemoveById 942100 - POST 204

One quirk of 2.6.1, documented in docs/WAF.md: the first flush is forwarded only after the handler wrote body bytes, so an endpoint that sends headers and then stays silent delivers them with its first event. Mainstream SSE servers send an opening event immediately.

Note: PR CI builds only the app Dockerfile; the edge image is built by the release workflow at tag time. The xcaddy build with 2.6.1 was done locally with Go 1.26.3 and is what the numbers above ran on. Users need the v1.4.9 edge image on every node - no config change.

coraza-caddy 2.5.0 propagates Flush() through a bare http.Flusher type
assertion. Caddy wraps the response writer in a responseRecorder whenever
access logging is on - which the panel enables on every node - and that
recorder only exposes flushing via Unwrap(), so every flush was dropped.
Any streaming response behind a WAF-enabled host (SSE, long-poll, chunked
progress) stalled until the buffer filled or the handler returned; the
browser never even got the headers. Blocking vs detection-only made no
difference, only disabling the WAF removed the wrapper.

Upstream fixed it in 2.6.1 ("flush and hijack through the response writer
Unwrap chain", corazawaf/coraza-caddy#344). coraza v3.7.0 and CRS 4.25.0
are unchanged, so the audit log the node-agent parses is unaffected.
2.6.1 needs Go 1.26; caddy:2.11.4-builder ships golang:1.26-alpine.

Verified with caddy 2.11.4 + the real generated route + srv0 access logs:
2.5.0 -> curl gets 0 SSE events, headless Chrome shows /api/realtime
pending forever on a Beszel hub; 2.6.1 -> events flow, dashboard
subscribes (204). Blocking mode still hits CRS 942100 on "systems/*",
which #15 covers.

Refs #14
@marcoome
marcoome merged commit 6ff02f4 into main Sep 16, 2026
2 checks passed
@marcoome
marcoome deleted the fix/edge-coraza-caddy-2.6.1 branch September 16, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant