Files
haproxy-manager-base/coraza-spoa/config.yaml
T
shadowdaoandClaude Opus 5.5 4f21bb2f76 feat(waf): make Coraza see request bodies; stamp complete/partial; 2026.10.2
Measured on haproxy 3.0.11 + coraza-spoa v0.7.1 + CRS 4.25.0 (local
haproxy-manager + coraza-spoa + echo backend):

- send-spoe-group fired at header time, so req.body held only what arrived
  with the headers. A 933180 payload whose body came in a second send(),
  2s late, or after Expect: 100-continue passed uninspected; 8 parallel
  14 KB payload-first POSTs: 6/40 blocked before, 40/40 after.
- Add `option http-buffer-request` (Coraza block only). Uploads are not
  held: one buffer is inspected, the rest streams (50 MB upload: sha-ok,
  same timing before/after).
- Stamp X-WHP-WAF-Body: complete|partial for Coraza (client copies
  stripped first, stripped again before the backend). Content-Length:
  exact compare; chunked/no-CL: body+headers < 12 KB => complete. WHP's
  generated rules use it to deny body-parse errors (200002/200003) only on
  complete bodies.
- Pin tune.bufsize 16384 (= default) when Coraza is on. Coraza costs
  ~3.3 ms/KB of body and the 100 ms SPOE timeout fails open: at 32 KB a
  payload at byte 0 of a 30 KB base64 body bypassed. SPOP frames > 65535
  are refused by the agent library.
- coraza-spoa/config.yaml: SecRequestBodyLimit 131072 / InMemory 131072 /
  ProcessPartial (Reject 413s even under DetectionOnly).
- Access log gains wafb= (complete|partial) and wafe= (SPOE error =
  request passed uninspected).
- overrides.conf: document (comment only) that the 920440 promotion turns
  the engine On for every URL with a file extension.
- scripts/test-waf-body-inspection.py (16 tests), wired into the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:49:27 -07:00

91 lines
3.9 KiB
YAML

# Coraza-SPOA configuration for WHP haproxy-manager integration.
#
# One named application "haproxy" — the haproxy-manager spoe template
# references this same name in its spoe-agent block, so the SPOA knows
# which rules to apply when HAProxy dispatches a request.
#
# Mode: SecRuleEngine DetectionOnly globally; overrides.conf promotes
# specific high-confidence rule ID ranges to enforcement individually.
# This is the safest posture for v1 — every rule logs, but only the
# unambiguous ones (scanner UAs, RCE, LFI, webshells, Log4Shell) block.
bind: 0.0.0.0:9000
# Process-level logging (separate from per-request audit logging below)
log_level: info
log_file: /dev/stdout
log_format: json
# Fallback when the request doesn't match a named application — we only
# have one, so it's also the default.
default_application: haproxy
applications:
- name: haproxy
directives: |
# CRS-bundled defaults: recommended Coraza settings + CRS setup +
# the rule pack itself (~16 MB of rules embedded in the binary).
Include @coraza.conf-recommended
# Request-body limits, set explicitly instead of inheriting the
# recommended file's 12.5 MB / Reject. HAProxy only ever hands us one
# buffer of body (tune.bufsize 16384 in haproxy-manager's
# hap_header.tpl), and the SPOP library refuses frames over 65535
# bytes, so no body we receive can exceed 64 KB. The invariant is:
# tune.bufsize (16 KB) < SPOP frame cap (65535) < SecRequestBodyLimit
# Breaking it is NOT harmless: SecRequestBodyLimitAction Reject
# interrupts with 413 even under DetectionOnly (Coraza
# setAndReturnBodyLimitInterruption ignores the engine mode), which the
# edge turns into a 403 on every large upload. ProcessPartial is the
# belt to that brace. InMemoryLimit == BodyLimit keeps the body buffer
# off disk (this image is distroless). Pinned by
# haproxy-manager-base/scripts/test-waf-body-inspection.py.
SecRequestBodyLimit 131072
SecRequestBodyInMemoryLimit 131072
SecRequestBodyLimitAction ProcessPartial
Include @crs-setup.conf.example
# Runtime-managed PRE-CRS exclusions written by WHP UI. Empty by default.
# Loaded BEFORE the CRS rules so per-host ctl:ruleRemoveById exemptions
# fire in phase:1 BEFORE the CRS rule they're trying to exempt would
# otherwise match. Server-wide overrides live in local-overrides.conf
# (loaded after CRS) instead.
Include /etc/coraza/pre-overrides.conf
Include @owasp_crs/*.conf
# WHP-specific overrides — day-one enforce list, plus tuning for
# the customer mix (WordPress, WooCommerce, Divi). Read this file
# to see exactly what blocks vs what's detect-only.
Include /etc/coraza/overrides.conf
# Runtime-managed POST-CRS overrides written by WHP UI. Empty by default.
Include /etc/coraza/local-overrides.conf
# Global mode: log all alerts, block only what overrides.conf
# explicitly promotes via ctl:ruleEngine=On.
SecRuleEngine DetectionOnly
# Audit log: JSON to a bind-mounted file so AI Monitor + log
# rotation can pick it up. RelevantOnly means we don't log every
# passing request, only ones that triggered at least one rule.
SecAuditEngine RelevantOnly
SecAuditLog /var/log/coraza/audit.log
SecAuditLogFormat JSON
SecAuditLogParts ABIJDEFHKZ
# HAProxy sends request-only events for v1. Response inspection adds
# latency on every page render with marginal additional protection
# for our customer mix; can be turned on later if we want it.
response_check: false
# Transactions cache for 60s. SPOE protocol is fire-and-forget per
# request, so this is just how long Coraza holds context for any
# multi-stage processing.
transaction_ttl_ms: 60000
log_level: info
log_file: /var/log/coraza/spoa.log
log_format: json