Breaking Changes
As outlined in the 0.1.0 release, every minor version before 1.0.0 may contain breaking changes.
Please review the sections below carefully before upgrading to avoid service interruptions.
1) SPOE execution now uses spoe-groups (explicit send-spoe-group)
What changed
SPOE execution is now driven by HAProxy spoe-groups and explicit calls to:
http-request send-spoe-group crowdsec <group-name>
This replaces relying on SPOE “event hooks” in the SPOE configuration.
Why this changed
This gives operators full control over when SPOE messages are sent, and allows you to set/override the source IP before the request is evaluated by the SPOA.
Who is impacted
All users upgrading to v0.3.0.
Required migration
-
Update your HAProxy config to explicitly call the relevant SPOE group(s) in your
frontend(s). -
Standardize on the upstream
crowdsec.cfgand stop relying on custom SPOE config logic (for example forcingreq.hdr_ip()usage).- Use the upstream config:
https://github.com/crowdsecurity/cs-haproxy-spoa-bouncer/blob/main/config/crowdsec.cfg
- Use the upstream config:
Example
frontend test
mode http
bind *:9090
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg
# Ensure HAProxy sees the correct client IP before calling the SPOE group
http-request set-src hdr_ip(X-Real-IP) if { req.hdr(X-Real-IP) -m found }
# Explicitly trigger the SPOE group at the right point in the request lifecycle
http-request send-spoe-group crowdsec crowdsec-http-body if body_within_limit || !{ req.body_size -m found }Operational note
You now control SPOE evaluation placement explicitly. If you previously relied on event hooks, ensure you add send-spoe-group to every relevant frontend and at the appropriate stage(s).
2) Captcha now requires a persistent signing_key (minimum 32 bytes)
What changed
Captcha moved away from purely server-side in-memory session state and now relies on signed tokens (JWT). As a result, a signing_key is now required.
Why this changed
Previously, restarting the SPOA invalidated in-memory captcha state, forcing users to re-complete captcha regardless of TTLs. Signed tokens allow captcha state to remain verifiable across restarts.
Who is impacted
All users with captcha enabled.
Required migration
Add signing_key to your captcha configuration (minimum 32 bytes):
hosts:
- host: "*.example.com"
captcha:
site_key: "123"
secret_key: "456"
provider: "hcaptcha"
timeout: 10 # HTTP client timeout in seconds (default: 5)
pending_ttl: "30m" # TTL for pending captcha tokens (default: 30m)
passed_ttl: "24h" # TTL for passed captcha tokens (default: 24h)
signing_key: "your-32-byte-minimum-secret-key-here" # REQUIRED in 0.3.0
- host: "*"
captcha:
fallback_remediation: allowChange redirection logic to use %[url] instead of %[var(txn.crowdsec.redirect)]
http-request redirect code 302 location %[url] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }note we found an issue for some users where without defining the following settings, the validation requests were being rejected
defaults
option http-buffer-requestMulti-SPOA / HA setups
If you run multiple SPOA instances serving the same domains, use the same signing_key across all deployments so tokens validate consistently.
Key generation
openssl rand -hex 32New Features
AppSec (WAF evaluation via SPOA)
The SPOA bouncer can now forward requests to CrowdSec AppSec for WAF evaluation, enabling HAProxy to provide:
- IP-based remediation (ban/captcha/allow)
- Request filtering via AppSec rules (WAF)
Docs:
- Intro: https://docs.crowdsec.net/docs/next/appsec/intro
- Quickstart: https://docs.crowdsec.net/docs/next/appsec/quickstart/general_setup
Enable AppSec forwarding in the SPOA bouncer
After following the quickstart, configure the SPOA bouncer with a global AppSec endpoint and optional per-host overrides:
# Global AppSec URL (optional)
api_key: 12345
appsec_url: http://127.0.0.1:7422
hosts:
- host: "*"
appsec:
always_send: false # Only validate if not already banned/captcha'd
# url: http://custom-appsec:7422 # Optional per-host override
# api_key: custom-key # Optional per-host overrideBehavior note
With always_send: false, AppSec evaluation is skipped when a request already has a higher-priority remediation outcome (for example, ban or captcha). This reduces unnecessary WAF calls and latency.
AppSec limitations and required HAProxy settings
Because HAProxy is optimized for high-throughput proxying, request body inspection through SPOE/SPOP has hard constraints.
Body access is limited by tune.bufsize
You have limited access to request bodies via tune.bufsize. The value can only go up to 65536 (64KB), due to an underlying library limitation:
global
log stdout format raw local0
tune.bufsize 65536 # 64KB - increased for WAF body inspectionRequest buffering must be enabled
To allow body inspection/forwarding, enable request buffering in defaults or the relevant frontend:
defaults
option http-buffer-requestChoosing the correct SPOE group
Two SPOE groups are provided to make performance vs inspection explicit:
-
crowdsec-http-body- Sends request body (when available/within limits)
- Required if you use captcha (captcha validation requires body-capable handling)
-
crowdsec-http-no-body- Avoids body forwarding for lower overhead
- Suitable for “phase 1” WAF checks and IP remediation when you do not need body inspection
Recommended layered approach
If you want full-depth inspection:
- Use HAProxy SPOA for IP remediation and phase 1 WAF signals (often
crowdsec-http-no-body) - Use a downstream component (for example, the Nginx remediation component) for full body inspection and deeper WAF enforcement
This keeps HAProxy fast while enabling deeper inspection where it is most effective.
What’s Changed
- feat: refactor captcha to stateless JWT-based design (#146) @LaurenceJJones
- CI: docker build (#132) @mmetc
- feat(prometheus): Add duration metrics for SPOA message processing (#135) @LaurenceJJones
- feat(spoe): Migrate implementation to dropmorepackets/haproxy-go (#138) @LaurenceJJones
- fix(prometheus): add graceful shutdown for server (#137) @LaurenceJJones
- feat(dockerfile): migrate to scratch-based image (#143) @LaurenceJJones
- feat(appsec): implementation and tests (#63) @LaurenceJJones