π Documentation π Hub π¬ Discourse
A Remediation Component for haproxy.
What it does ?β
The cs-haproxy-spoa-bouncer allows CrowdSec to enforce blocking, CAPTCHA, or
allow actions directly within HAProxy using the SPOE
protocol.
This remediation component is meant to obsolete the old lua-based haproxy bouncer.
It supports IP-based decisions, CAPTCHA challenges, GeoIP-based headers, and integrates cleanly with CrowdSecβs LAPI using the stream bouncer protocol.
Supported features:
- Stream mode (pull the local API for new/old decisions every X seconds)
- Ban remediation (can ban an IP address by redirecting or returning a custom HTML page)
- Captcha remediation (can return a captcha)
- Works with IPv4/IPv6
- Support IP ranges (can apply a remediation on an IP range)
- We are working on supporting AppSec
Installationβ
We strongly encourage the use of our packages.
Using packagesβ
You will have to setup crowdsec repositories first setup crowdsec repositories.
- Debian/Ubuntu
- RHEL/Centos/Fedora
sudo apt install crowdsec-haproxy-spoa-bouncer
sudo dnf install crowdsec-haproxy-spoa-bouncer
Bouncer configurationβ
If you are using packages, and have a lapi on the same server the following
configuration file /etc/crowdsec/bouncer/crowdsec-spoa-bouncer.yaml should
already be in a working state, and can skip this section and begin with HAProxy
Configuration.
If your CrowdSec Engine is installed on an other server, you'll have to update
the /etc/crowdsec/bouncer/crowdsec-spoa-bouncer.yaml
file.
HAProxy Configurationβ
HAProxy requires two configuration files for integration with the bouncer. The
primary file is /etc/haproxy/haproxy.cfg, which must be modified to enable
communication with the SPOE engineβour documentation will guide you through
this. The second file is /etc/haproxy/crowdsec.cfg, which contains the SPOE
agent configuration. This file is automatically installed along with the bouncer
package on the condition that /etc/haproxy exists.
If you are using packages, you will find the haproxy configuration
snippets in /usr/share/doc/crowdsec-haproxy-spoa-bouncer/examples.
SPOE Filterβ
Add a SPOE agent configuration to /etc/haproxy/crowdsec.cfg:
/etc/haproxy/crowdsec.cfg
[crowdsec]
spoe-agent crowdsec-agent
messages crowdsec-ip crowdsec-http
option var-prefix crowdsec
option set-on-error error
timeout hello 100ms
timeout idle 30s
timeout processing 500ms
use-backend crowdsec-spoa
log global
## This message is used to customise the remediation from crowdsec-ip based on the host header
spoe-message crowdsec-http
args remediation=var(txn.crowdsec.remediation) crowdsec_captcha_cookie=req.cook(crowdsec_captcha_cookie) id=unique-id host=hdr(Host) method=method path=path query=query version=req.ver headers=req.hdrs body=req.body url=url ssl=ssl_fc
event on-frontend-http-request
## This message should be the first to trigger in the chain
spoe-message crowdsec-ip
args id=unique-id src-ip=src src-port=src_port
event on-client-session
If you installed the haproxy spoe bouncer through package, you will find this
configuration file in /usr/share/docs/crowdsec-haproxy-spoa-bouncer/examples
This crowdsec spoe agent configuration is then referenced in the main haproxy
configuration file /etc/haproxy/haproxy.cfg and may be added at the bottom of
the haproxy configuration file.
/etc/haproxy/haproxy.cfg
[...]
frontend http-in
bind *:80
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg
http-request set-header X-CrowdSec-Remediation %[var(txn.crowdsec.remediation)]
## Handle 302 redirect for successful captcha validation (native HAProxy redirect)
http-request redirect code 302 location %[var(txn.crowdsec.redirect)] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }
## Call lua script only for ban and captcha remediations (performance optimization)
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "captcha" }
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "ban" }
## Handle captcha cookie management via HAProxy (new approach)
## Set captcha cookie when SPOA provides captcha_status (pending or valid)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_status) -m found } { var(txn.crowdsec.captcha_cookie) -m found }
## Clear captcha cookie when cookie exists but no captcha_status (Allow decision)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_cookie) -m found } !{ var(txn.crowdsec.captcha_status) -m found }
use_backend <whatever>
backend crowdsec-spoa
mode tcp
server s1 127.0.0.1:9000
In the global section of your haproxy.cfg, lua path configuration is also mandatory:
global
[...]
lua-load /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/crowdsec.lua
An example that includes this snippet can also be found in
/usr/share/docs/crowdsec-haproxy-spoa-bouncer/examples/haproxy.cfg.
Specific featuresβ
To enable CAPTCHA for a domain:β
hosts:
- host: "example.com"
captcha:
site_key: "<your-site-key>"
secret_key: "<your-secret-key>"
provider: "hcaptcha"
The following captcha providers are supported:
hcaptcha
recaptcha
turnstile
HAProxy Behind a CDNβ
When HAProxy is deployed behind an upstream Content Delivery Network (CDN), the source IP seen by HAProxy will be the CDN's edge server IP, not the real client IP. To properly evaluate and apply security rules based on the actual client IP, you need to configure the SPOA to extract the real IP from the CDN-provided header.
Configuration Changesβ
When HAProxy is behind a CDN, modify your /etc/haproxy/crowdsec.cfg to:
- Use only the
crowdsec-httpmessage (thecrowdsec-ipmessage will capture the CDN edge IP, which is not useful) - Extract the real client IP from the CDN header using
req.hdr_ip()to convert it to HAProxy's IP type - Pass the real IP to the bouncer via the SPOE message
/etc/haproxy/crowdsec.cfg (CDN Configuration)
# /etc/haproxy/spoe/crowdsec.cfg
# SPOE section for CDN deployments
# - Uses a single message: crowdsec-http
# - Extracts real client IP from X-Real-IP header (adjust if needed)
# - Falls back to IP remediation if 'remediation' var is not set
[crowdsec]
spoe-agent crowdsec-agent
messages crowdsec-http
option var-prefix crowdsec
option set-on-error error
timeout hello 100ms
timeout idle 30s
timeout processing 500ms
use-backend crowdsec-spoa
log global
# This message extracts the real IP via X-Real-IP and includes all arguments.
# IMPORTANT: req.hdr_ip() returns an IP type (required by SPOE protocol).
# If 'remediation' isn't provided by HAProxy, the bouncer will check IP remediation.
spoe-message crowdsec-http
args remediation=var(txn.crowdsec.remediation) \
crowdsec_captcha_cookie=req.cook(crowdsec_captcha_cookie) \
id=unique-id host=hdr(Host) method=method path=path query=query \
version=req.ver headers=req.hdrs body=req.body url=url ssl=ssl_fc \
src-ip=req.hdr_ip(x-real-ip) src-port=src_port
event on-frontend-http-request
Key Changes Explainedβ
- Single message: Only
crowdsec-httpis used. Thecrowdsec-ipmessage would run aton-client-sessionand capture the CDN's IP, not the real client IP, so it's omitted. - IP extraction: The
req.hdr_ip(x-real-ip)function extracts the IP from theX-Real-IPheader and converts it to HAProxy's IP type, which is required by the SPOE protocol. - Header name: If your CDN uses a different header (e.g.,
X-Forwarded-For,CF-Connecting-IPfor Cloudflare), adjust the header name accordingly. For Cloudflare specifically, usereq.hdr_ip(cf-connecting-ip).
HAProxy Configurationβ
Your /etc/haproxy/haproxy.cfg frontend configuration remains mostly the same, but ensure the CDN header is being passed through:
frontend http-in
bind *:80
# Ensure the CDN header is preserved (may already be done by your CDN)
# You can optionally add debugging with set-header
# http-request set-header X-Real-IP %[req.hdr(X-Real-IP)]
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg
http-request set-header X-CrowdSec-Remediation %[var(txn.crowdsec.remediation)]
## Handle 302 redirect for successful captcha validation (native HAProxy redirect)
http-request redirect code 302 location %[var(txn.crowdsec.redirect)] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }
## Call lua script only for ban and captcha remediations (performance optimization)
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "captcha" }
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "ban" }
## Handle captcha cookie management via HAProxy (new approach)
## Set captcha cookie when SPOA provides captcha_status (pending or valid)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_status) -m found } { var(txn.crowdsec.captcha_cookie) -m found }
## Clear captcha cookie when cookie exists but no captcha_status (Allow decision)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_cookie) -m found } !{ var(txn.crowdsec.captcha_status) -m found }
use_backend <whatever>
backend crowdsec-spoa
mode tcp
server s1 127.0.0.1:9000
Common CDN Headersβ
| CDN Provider | Header Name | HAProxy Function |
|---|---|---|
| Generic / Most CDNs | X-Real-IP | req.hdr_ip(x-real-ip) |
| Cloudflare | CF-Connecting-IP | req.hdr_ip(cf-connecting-ip) |
| AWS CloudFront | CloudFront-Viewer-Address | req.hdr_ip(cloudfront-viewer-address) |
| Akamai | True-Client-IP | req.hdr_ip(true-client-ip) |
| Azure CDN | X-Forwarded-For | req.hdr_ip(x-forwarded-for) |
Prometheus Metricsβ
Enable and expose metrics:
prometheus:
enabled: true
listen_addr: 127.0.0.1
listen_port: 60601
Access them at http://127.0.0.1:60601/metrics.
Admin Socketβ
You can query the bouncer runtime state using the admin socket:
socat - UNIX-CONNECT:/run/crowdsec-spoa-admin.sock
Commands:
get hosts
get host <host> session <uuid> <key>
set host <host> session <uuid> <key> <value>
get ip <ip>
val host <host> cookie <cookie>
val host <host> captcha <response>
Manual installation and advanced configurationβ
We strongly encourage the use of our packages.
Compile the Binaryβ
This requires a whole working golang installation.
git clone https://github.com/crowdsecurity/crowdsec-spoa-bouncer.git
cd crowdsec-spoa-bouncer
make build
Configure the Bouncerβ
sudo mkdir -p /etc/crowdsec/bouncers/
sudo cp config/crowdsec-spoa-bouncer.yaml /etc/crowdsec/bouncers/
You can always edit the configuration file at /etc/crowdsec/bouncer/crowdsec-spoa-bouncer.yaml:
/etc/crowdsec/bouncer/crowdsec-spoa-bouncer.yaml
log_mode: file
log_dir: /var/log/
log_level: info
log_compression: true
log_max_size: 100
log_max_backups: 3
log_max_age: 30
update_frequency: 10s
api_url: http://127.0.0.1:8080/
api_key: ${API_KEY}
insecure_skip_verify: false
workers:
- name: spoa1
listen_addr: 0.0.0.0:9000
listen_socket: /run/crowdsec-spoa/spoa-1.sock
worker_user: crowdsec-spoa
worker_group: crowdsec-spoa
asn_database_path: /var/lib/crowdsec/data/GeoLite2-ASN.mmdb
city_database_path: /var/lib/crowdsec/data/GeoLite2-City.mmdb
admin_socket: /run/crowdsec-spoa-admin.sock
prometheus:
enabled: true
listen_addr: 127.0.0.1
listen_port: 60601
You can get a workable configuration by using the yaml above and getting and api key by:
sudo cscli bouncers add mybouncer
API key for 'bouncertest':
JdVa7DKBM35gPDAR014pH/55l38fxLGt02NPPnZgLQI
Please keep this key since you will not be able to retrieve it!
- Paste the key into:
api_key: your-generated-key
In the /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml file the following
keys are of some importance:
- Set your LAPI URL to point to your CrowdSec LAPI instance:
api_url: http://127.0.0.1:8080/
You can check that the bouncer is correctly installed with cscli:
β― sudo cscli bouncers list
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Name IP Address Valid Last API pull Type
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
cs-spoa-bouncer-1752052534 127.0.0.1 βοΈ crowdsec-spoa-bouncer
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β― sudo cscli bouncers inspect cs-spoa-bouncer-1752052534
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Bouncer: cs-spoa-bouncer-1752052534
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Created At 2025-07-09 09:15:34.685444393 +0000 UTC
Last Update 2025-07-09 12:42:18.92023029 +0000 UTC
Revoked? false
IP Address 127.0.0.1
Type crowdsec-spoa-bouncer
Version v0.0.3-beta29-rpm-pragmatic-arm64-db7065289a0f5ce1c92f34807c9a98b23c07dc90
Last Pull
Auth type api-key
OS ?
Auto Created false
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Create runtime socket directory and crowdsec-spoa user:
sudo
sudo mkdir -p /run/crowdsec-spoa
sudo chown crowdsec-spoa:crowdsec-spoa /run/crowdsec-spoa
Configure HAProxyβ
Lua Integration & Environment Variablesβ
In the global section of your haproxy.cfg, configure Lua paths and template environment:
global
lua-prepend-path /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/?.lua
lua-load /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/crowdsec.lua
setenv CROWDSEC_BAN_TEMPLATE_PATH /var/lib/crowdsec/lua/haproxy/templates/ban.html
setenv CROWDSEC_CAPTCHA_TEMPLATE_PATH /var/lib/crowdsec/lua/haproxy/templates/captcha.html
These variables are used by the Lua module to render proper HTML responses for banned or captcha-validated users.
Add SPOE Filter in frontendβ
frontend test
mode http
bind *:9090
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg
http-request set-header X-CrowdSec-Remediation %[var(txn.crowdsec.remediation)] if { var(txn.crowdsec.remediation) -m found }
http-request set-header X-CrowdSec-IsoCode %[var(txn.crowdsec.isocode)] if { var(txn.crowdsec.isocode) -m found }
## Handle 302 redirect for successful captcha validation (native HAProxy redirect)
http-request redirect code 302 location %[var(txn.crowdsec.redirect)] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }
## Call lua script only for ban and captcha remediations (performance optimization)
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "captcha" }
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "ban" }
## Handle captcha cookie management via HAProxy (new approach)
## Set captcha cookie when SPOA provides captcha_status (pending or valid)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_status) -m found } { var(txn.crowdsec.captcha_cookie) -m found }
## Clear captcha cookie when cookie exists but no captcha_status (Allow decision)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_cookie) -m found } !{ var(txn.crowdsec.captcha_status) -m found }
use_backend test_backend
Create SPOE Configβ
Create /etc/haproxy/crowdsec.cfg:
/etc/haproxy/crowdsec.cfg
spoe-agent crowdsec-agent
messages crowdsec-ip crowdsec-http
option var-prefix crowdsec
option set-on-error error
timeout hello 100ms
timeout idle 30s
timeout processing 500ms
use-backend crowdsec-spoa
spoe-message crowdsec-ip
args id=unique-id src-ip=src src-port=src_port
event on-client-session
spoe-message crowdsec-http
args remediation=var(txn.crowdsec.remediation) crowdsec_captcha_cookie=req.cook(crowdsec_captcha_cookie) id=unique-id host=hdr(Host) method=method path=path query=query version=req.ver headers=req.hdrs body=re
q.body url=url ssl=ssl_fc
event on-frontend-http-request
Add SPOE Backendβ
backend crowdsec-spoa
mode tcp
balance roundrobin
server s1 127.0.0.1:9000
Modify HAProxy systemd Unit (Optional)β
Edit /etc/systemd/system/haproxy.service and add:
[Service]
Environment=CROWDSEC_BAN_TEMPLATE_PATH=/var/lib/crowdsec/lua/haproxy/templates/ban.html
Environment=CROWDSEC_CAPTCHA_TEMPLATE_PATH=/var/lib/crowdsec/lua/haproxy/templates/captcha.html
Then reload systemd:
sudo systemctl daemon-reexec
sudo systemctl daemon-reload
Start the Bouncerβ
Run Directly
sudo ./crowdsec-spoa-bouncer -c /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml
Or Run as a Systemd Service
sudo cp config/crowdsec-spoa-bouncer.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now crowdsec-spoa-bouncer