Files
haproxy-manager-base/templates/hap_coraza_spoe_engine.tpl
T
Claude b2f835a88c
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 1m19s
fix(logging): capture the full User-Agent, drop per-request SPOE log noise
Two defects caught by watching the real production access log on whp01 in the
minutes after 2026.08.7 made access logging work for the first time.

1. User-Agent was being truncated to its tail.

   `http-request capture req.hdr(User-Agent)` treats the header as a
   comma-separated list and returns only the LAST element. Real User-Agent
   strings contain commas, so

     Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
     (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36

   logged as

     ua=like Gecko) Chrome/131.0.0.0 Safari/537.36

   losing the platform half -- exactly the half needed to tell a spoofed
   crawler from a real browser, which is one of the main reasons the field was
   added. Switched to req.fhdr(), which returns the full unsplit header value.

2. SPOE was writing one log line per inspected request.

   `log global` inside the spoe-agent block emitted

     SPOE: [coraza] <GROUP:coraza-req> sid=537 st=0 0/0/0/0/0 32/32 0/0 0/467

   for every single request. Measured on whp01: 618 SPOE lines against 669 real
   access lines -- ~48% of the log volume, roughly doubling the edge's log
   footprint (~400 MB/day extra) to record `st=0` over and over.

   It carries nothing incident response needs. The WAF verdict is already in
   the access line (status 403 plus the id= UUID, which joins to
   /var/log/coraza/audit.log for the rule_id), and per-transaction WAF detail
   is written by the SPOA itself to /var/log/coraza/spoa.log. Agent-level
   failures still surface through `option set-on-error error` ->
   var(txn.coraza.error) and the fail-open path in hap_listener.tpl.

Verified: scripts/validate-rendered-config.py passes `haproxy -c` on both the
"default" and "full" scenarios against the real 3.0.11 binary; wp-admin gate,
trusted-proxy gate and xmlrpc rate-limit suites all still pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 10:17:07 -07:00

76 lines
3.7 KiB
Smarty

# Coraza SPOE engine configuration.
#
# Written to /etc/haproxy/coraza-spoe.cfg by haproxy_manager.generate_config()
# when HAPROXY_CORAZA_SPOE_BACKEND env var is set. Referenced from haproxy.cfg
# 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; 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
# `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 variables the agent sets back on the request transaction —
# e.g. var(txn.coraza.error) when set-on-error triggers.
option var-prefix coraza
# 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
timeout hello 2s
timeout idle 2m
timeout processing 100ms
use-backend coraza-spoa-backend
# NO `log global` here, deliberately.
#
# `log global` in a spoe-agent emits one line PER INSPECTED REQUEST, e.g.
# SPOE: [coraza] <GROUP:coraza-req> sid=537 st=0 0/0/0/0/0 32/32 0/0 0/467
# Measured on whp01 immediately after access logging started working:
# 618 SPOE lines vs 669 real access lines -- it was ~48% of the log volume,
# i.e. it would roughly DOUBLE the edge's log footprint (~400 MB/day extra)
# to record `st=0` over and over.
#
# It carries nothing incident response needs: the WAF's verdict is already
# visible in the access log line (status 403 + the `id=` UUID, which joins
# to /var/log/coraza/audit.log for the rule_id), and per-transaction WAF
# detail is written by the SPOA itself to /var/log/coraza/spoa.log.
# Agent-level failures still surface via `option set-on-error error` ->
# var(txn.coraza.error) and the fail-open path in hap_listener.tpl.
# 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.
#
# src-ip uses var(txn.real_ip) — HAProxy resolves the real client IP at the
# top of the frontend (CF-Connecting-IP > X-Real-IP > X-Forwarded-For > src,
# only honoring those headers from trusted proxies). Falls back to `src`
# when no proxy headers are present. This is what shows up as `client_ip`
# in /var/log/coraza/audit.log and the rule manager's view-matches panel.
spoe-message coraza-req
args app=str(haproxy) src-ip=var(txn.real_ip) 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