π Documentation π Hub π¬ Discourse
A remediation component for HAProxy
Beta Remediation Component, please report any issues on GitHub
This is the only HAProxy bouncer with AppSec support. After installing the bouncer, enable the AppSec (WAF) Component for real-time WAF protection, virtual patching, and defense against known CVEs.
Follow the dedicated AppSec Quickstart for HAProxy β it picks up right where this page ends.
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 LAPI decisions periodically)
- mTLS to LAPI (via
cert_path/key_path/ca_cert_path) - IP / range / country decisions
- Ban remediation (custom HTML / redirects)
- CAPTCHA remediation (hCaptcha / reCAPTCHA / Turnstile)
- GeoIP headers (ASN / Country)
- AppSec (WAF evaluation via CrowdSec AppSec)
- Prometheus metrics
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
Containerβ
The container image runs the SPOA bouncer (it does not bundle HAProxy): crowdsecurity/spoa-bouncer.
The container examples below are not a complete HAProxy setup. For production, pin HAProxy to a stable version (rather than :latest) and adapt haproxy.cfg to your environment (TLS, backends, logging, timeouts, etc.).
Quick start:
docker run -d \
--name crowdsec-spoa-bouncer \
-e CROWDSEC_KEY="<your-lapi-api-key>" \
-e CROWDSEC_URL="http://crowdsec:8080/" \
-p 9000:9000 \
-p 6060:6060 \
crowdsecurity/spoa-bouncer
If HAProxy runs in another container (for example in Docker Compose), point the SPOA backend to crowdsec-spoa-bouncer:9000.
Docker Compose exampleβ
services:
crowdsec:
image: crowdsecurity/crowdsec:latest
restart: unless-stopped
ports:
- 127.0.0.1:8080:8080
environment:
COLLECTIONS: "crowdsecurity/haproxy"
BOUNCER_KEY_SPOA: "${BOUNCER_KEY_SPOA}"
GID: "${GID-1000}"
volumes:
- crowdsec-db:/var/lib/crowdsec/data/
- crowdsec-config:/etc/crowdsec/
# Optional: configure log acquisition for your setup
# - ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro
networks:
- crowdsec
crowdsec-spoa-bouncer:
image: crowdsecurity/spoa-bouncer:latest
restart: unless-stopped
depends_on:
- crowdsec
environment:
CROWDSEC_KEY: "${BOUNCER_KEY_SPOA}"
CROWDSEC_URL: "http://crowdsec:8080/"
volumes:
- templates:/var/lib/crowdsec-haproxy-spoa-bouncer/html/
- lua:/usr/lib/crowdsec-haproxy-spoa-bouncer/lua/
networks:
- crowdsec
haproxy:
image: haproxy:latest
restart: unless-stopped
volumes:
- ./config/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
- ./config/crowdsec.cfg:/etc/haproxy/crowdsec.cfg:ro
- templates:/var/lib/crowdsec-haproxy-spoa-bouncer/html/:ro
- lua:/usr/lib/crowdsec-haproxy-spoa-bouncer/lua/:ro
ports:
- "80:80"
- "443:443"
depends_on:
- crowdsec-spoa-bouncer
networks:
- crowdsec
volumes:
crowdsec-db:
crowdsec-config:
lua:
templates:
networks:
crowdsec:
Create ./config/haproxy.cfg and ./config/crowdsec.cfg from the βHAProxy Configurationβ section below (in Compose, the SPOA backend server should target crowdsec-spoa-bouncer:9000). Set BOUNCER_KEY_SPOA in a .env file or your shell environment, and persist CrowdSec directories (at least /var/lib/crowdsec/data/) as described in the Docker getting started guide.
To use a custom configuration file:
docker run -d \
--name crowdsec-spoa-bouncer \
-v $PWD/crowdsec-spoa-bouncer.yaml:/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml:ro \
-p 9000:9000 \
crowdsecurity/spoa-bouncer
If you run HAProxy without the crowdsec-haproxy-spoa-bouncer package, you still need the Lua scripts and HTML templates. They are shipped in the image at /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/ and /var/lib/crowdsec-haproxy-spoa-bouncer/html/ and can be copied/mounted into your HAProxy environment.
For all container options and environment variables, see: https://github.com/crowdsecurity/cs-haproxy-spoa-bouncer/blob/main/docker/README.md
Bouncer configurationβ
If you are using packages, and have a lapi on the same server the following
configuration file /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml should
already be in a working state, and you can skip this section and begin with HAProxy
Configuration.
If your CrowdSec Engine is installed on another server, you'll need to update
the /etc/crowdsec/bouncers/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-tcp
groups crowdsec-http-body crowdsec-http-no-body
option var-prefix crowdsec
option set-on-error error
timeout hello 200ms
timeout idle 55s
timeout processing 500ms
use-backend crowdsec-spoa
log global
## TCP/IP level check - runs early to check IP remediation
## Uses event directive to trigger on each new client session (not sent as a group)
spoe-message crowdsec-tcp
args id=unique-id src-ip=src src-port=src_port
event on-client-session
## HTTP message with body - used when body size is within limit for AppSec
## Note: Host and captcha cookie are extracted from headers=req.hdrs, no need to send separately
spoe-message crowdsec-http-body
args remediation=var(txn.crowdsec.remediation) id=unique-id method=method path=path query=query version=req.ver headers=req.hdrs body=req.body url=url ssl=ssl_fc src-ip=src src-port=src_port
## HTTP message without body - used when body is too large or not needed
## Note: Host and captcha cookie are extracted from headers=req.hdrs, no need to send separately
spoe-message crowdsec-http-no-body
args remediation=var(txn.crowdsec.remediation) id=unique-id method=method path=path query=query version=req.ver headers=req.hdrs url=url ssl=ssl_fc src-ip=src src-port=src_port
## Group for HTTP message with body - used when body size is within limit for AppSec
spoe-group crowdsec-http-body
messages crowdsec-http-body
## Group for HTTP message without body - used when body is too large or not needed
spoe-group crowdsec-http-no-body
messages crowdsec-http-no-body
If you installed the haproxy spoe bouncer through package, you will find this
configuration file in /usr/share/doc/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
# Select which SPOE group to send (with/without body)
acl body_within_limit req.body_size -m int le 51200 # 50KB - stay safely under SPOE frame limit
http-request send-spoe-group crowdsec crowdsec-http-body if body_within_limit || !{ req.body_size -m found }
http-request send-spoe-group crowdsec crowdsec-http-no-body if !body_within_limit { req.body_size -m found }
http-request set-header X-Crowdsec-Remediation %[var(txn.crowdsec.remediation)]
## Handle 302 redirect for successful captcha validation (redirect to current request URL)
http-request redirect code 302 location %[url] 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-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-haproxy-spoa-bouncer/html/ban.html
setenv CROWDSEC_CAPTCHA_TEMPLATE_PATH /var/lib/crowdsec-haproxy-spoa-bouncer/html/captcha.html
An example that includes this snippet can also be found in
/usr/share/doc/crowdsec-haproxy-spoa-bouncer/examples/haproxy.cfg.
Real client IP behind a CDN (or upstream proxy)β
When HAProxy is deployed behind an upstream CDN/proxy, the source IP seen by HAProxy may be the CDN edge IP, not the real client IP. Set the source IP in HAProxy before calling send-spoe-group:
frontend http-in
# Extract real client IP from proxy headers (runs before SPOE groups)
# Priority: X-Real-IP > CF-Connecting-IP > X-Forwarded-For > direct src
http-request set-src hdr_ip(X-Real-IP) if { req.hdr(X-Real-IP) -m found }
http-request set-src hdr_ip(CF-Connecting-IP) if { req.hdr(CF-Connecting-IP) -m found } !{ req.hdr(X-Real-IP) -m found }
http-request set-src hdr_ip(X-Forwarded-For) if { req.hdr(X-Forwarded-For) -m found } !{ req.hdr(X-Real-IP) -m found } !{ req.hdr(CF-Connecting-IP) -m found }
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg
acl body_within_limit req.body_size -m int le 51200
http-request send-spoe-group crowdsec crowdsec-http-body if body_within_limit || !{ req.body_size -m found }
http-request send-spoe-group crowdsec crowdsec-http-no-body if !body_within_limit { req.body_size -m found }
In upstream-proxy/CDN setups, the TCP check (crowdsec-tcp) still runs at on-client-session and may see the proxy IP; calling an HTTP group after set-src ensures the request is evaluated with the real client IP.
If you rely on headers like X-Real-IP / X-Forwarded-For, ensure only your trusted upstream CDN/proxy can connect to your HAProxy ports (typically 80/443). Otherwise, attackers can connect directly and spoof these headers.
Common CDN headersβ
| CDN Provider | Header Name | HAProxy Function |
|---|---|---|
| Generic / Most CDNs | X-Real-IP | hdr_ip(X-Real-IP) |
| Cloudflare | CF-Connecting-IP | hdr_ip(CF-Connecting-IP) |
| AWS CloudFront | CloudFront-Viewer-Address | hdr_ip(CloudFront-Viewer-Address) |
| Akamai | True-Client-IP | hdr_ip(True-Client-IP) |
| Azure CDN | X-Forwarded-For | hdr_ip(X-Forwarded-For) |
If your CDN uses X-Forwarded-For with multiple IPs (comma-separated), you may need to select the right one:
http-request set-src hdr_ip(X-Forwarded-For,1) if { req.hdr(X-Forwarded-For) -m found }
If your CDN appends IPs from right to left, use -1 for the rightmost IP:
http-request set-src hdr_ip(X-Forwarded-For,-1) if { req.hdr(X-Forwarded-For) -m found }
How-to guidesβ
- CAPTCHA: enable per domain
- AppSec: forward requests for WAF evaluation
- Prometheus: expose metrics endpoint
Enable CAPTCHA for a domainβ
hosts:
- host: "example.com"
captcha:
site_key: "<your-site-key>"
secret_key: "<your-secret-key>"
provider: "hcaptcha"
signing_key: "<your-32-byte-minimum-secret-key>"
The following captcha providers are supported:
hcaptcha
recaptcha
turnstile
AppSec (WAF) forwarding β configuration referenceβ
To enable the WAF end-to-end, follow the AppSec Quickstart for HAProxy β it picks up after the bouncer install and walks through collections, acquisition, and enabling forwarding.
Relevant bouncer keys in /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml:
# Global AppSec URL (optional)
appsec_url: http://127.0.0.1:7422
appsec_timeout: 200ms
hosts:
- host: "*"
appsec:
always_send: false
# url: http://custom-appsec:7422 # optional per-host override
# api_key: custom-key # optional per-host override
HAProxy requirements when using AppSec (and/or captcha):
- Enable request buffering:
option http-buffer-request - Increase HAProxy buffer size (max 64KB):
tune.bufsize 65536 - Use the
crowdsec-http-bodygroup when the body is available (see thebody_within_limit+send-spoe-groupexample above)
Because request-body forwarding is constrained by HAProxy/SPOE/SPOP limits, keep an explicit body size limit (for example 51200) and consider a layered approach (IP remediation at HAProxy, deeper inspection downstream).
Expose 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.
Configuration Referenceβ
The upstream example configurations live in the cs-haproxy-spoa-bouncer repository:
crowdsec.cfg: https://github.com/crowdsecurity/cs-haproxy-spoa-bouncer/blob/main/config/crowdsec.cfg- HAProxy examples: https://github.com/crowdsecurity/cs-haproxy-spoa-bouncer/tree/main/config
YAML snippets below show each key in context.
log_modeβ
file|stdout
Where the log contents are written (With file it will be written to log_dir with the name crowdsec-spoa-bouncer.log)
log_mode: "file" # or "stdout"
log_dirβ
string
Log directory path that will contain the log file. By default, this should be set to /var/log/crowdsec-spoa/ as this directory is automatically created by the systemd service.
When installed from packages, the systemd unit runs the bouncer as the crowdsec-spoa user and creates /var/log/crowdsec-spoa/ automatically (via LogsDirectory=). If you set a custom log_dir, make sure the directory exists and that the crowdsec-spoa user has permission to read/write there.
log_dir: "/var/log/crowdsec-spoa/"
log_levelβ
trace|debug|info|warn|error
Log level (default: info)
log_level: "info"
compress_logsβ
true|false
Compress log files on rotation (default: true)
compress_logs: true
log_max_sizeβ
int (in MB)
Max size of log files before rotation (default: 500)
log_max_size: 500
log_max_filesβ
int
How many backup log files to keep before deletion (can happen before log_max_age is reached) (default: 3)
log_max_files: 3
log_max_ageβ
int (in days)
Max age of backup files before deletion (can happen before log_max_files is reached) (default: 30)
log_max_age: 30
The LAPI connection settings (api_url, update_frequency, insecure_skip_verify, api_key, mTLS paths, and decision filters) are read by the embedded stream bouncer.
update_frequencyβ
string (parseable by time.ParseDuration)
Frequency to contact the API for new/deleted decisions (default: 10s)
update_frequency: "10s"
api_urlβ
string
URL of the local API EG: http://127.0.0.1:8080
api_url: "https://lapi.example.com:8080/"
api_keyβ
string
API key to authenticate with the local API
api_key: "<your-lapi-api-key>"
insecure_skip_verifyβ
true|false
Skip verification of the API certificate, typical for self-signed certificates
insecure_skip_verify: false
cert_pathβ
string
Client certificate path for mTLS to LAPI.
cert_path: "/etc/ssl/certs/client.crt"
key_pathβ
string
Client private key path for mTLS to LAPI.
key_path: "/etc/ssl/private/client.key"
ca_cert_pathβ
string
CA certificate path for validating the LAPI certificate (mTLS / custom CAs).
ca_cert_path: "/etc/ssl/certs/ca.crt"
retry_initial_connectβ
true|false
Retry connecting to LAPI on startup instead of failing fast.
retry_initial_connect: true
scopesβ
[]string
Only pull decisions matching these scopes (for example ip, range, country).
scopes: ["ip", "range", "country"]
scenarios_containingβ
[]string
Only pull decisions whose scenario contains one of these strings.
scenarios_containing: ["crowdsecurity/"]
scenarios_not_containingβ
[]string
Do not pull decisions whose scenario contains one of these strings.
scenarios_not_containing: ["whitelist"]
originsβ
[]string
Only pull decisions from these origins.
origins: ["crowdsecurity", "lists"]
listen_tcpβ
string
TCP address and port to listen on for SPOE connections. Format: ip:port or :port
listen_tcp: "0.0.0.0:9000"
At least one of listen_tcp or listen_unix must be configured.
listen_unixβ
string
Unix socket path to listen on for SPOE connections
listen_unix: "/run/crowdsec-spoa/spoa.sock"
At least one of listen_tcp or listen_unix must be configured.
hostsβ
[]object
List of host configurations for domain-specific settings
hosts:
- host: "example.com"
captcha:
provider: "turnstile"
site_key: "<your-site-key>"
secret_key: "<your-secret-key>"
signing_key: "<your-32-byte-minimum-secret-key>"
ban:
contact_us_url: "https://example.com/support"
appsec:
always_send: false
log_level: "info"
- host: "*"
captcha:
fallback_remediation: "allow"
hostβ
string
Hostname pattern to match (supports wildcards).
Note: The list of host objects is automatically sorted from longest to shortest pattern, including wildcards. For example, *.example.com (matching all subdomains) will be evaluated before example.com, and the wildcard * (which matches any host) will always be at the bottom of the list. This ensures that more specific patterns take precedence over more general ones.
hosts:
- host: "*.example.com" # <-- host pattern
captchaβ
object
CAPTCHA configuration for this host
hosts:
- host: "example.com"
captcha:
provider: "turnstile"
site_key: "<your-site-key>"
secret_key: "<your-secret-key>"
signing_key: "<your-32-byte-minimum-secret-key>"
providerβ
hcaptcha|recaptcha|turnstile
CAPTCHA provider to use
hosts:
- host: "example.com"
captcha:
provider: "turnstile" # <-- provider
site_keyβ
string
CAPTCHA site key
hosts:
- host: "example.com"
captcha:
site_key: "<your-site-key>" # <-- site_key
secret_keyβ
string
CAPTCHA secret key
hosts:
- host: "example.com"
captcha:
secret_key: "<your-secret-key>" # <-- secret_key
fallback_remediationβ
string
ban|allow
If captcha is not configured which remediation to use as a fallback. Can be configured to allow to pass on captcha remediations (default: ban)
hosts:
- host: "*"
captcha:
fallback_remediation: "allow" # <-- fallback_remediation
timeoutβ
int (in seconds)
HTTP client timeout in seconds, maximum 300 (default: 5)
hosts:
- host: "example.com"
captcha:
timeout: 5 # <-- timeout (seconds)
cookieβ
object
Cookie generation configuration
hosts:
- host: "example.com"
captcha:
cookie:
secure: "auto"
http_only: true
secureβ
auto|always|never
Set the secure flag on the cookie. auto relies on the ssl_fc flag from HAProxy (default: auto)
hosts:
- host: "example.com"
captcha:
cookie:
secure: "auto" # <-- secure
http_onlyβ
true|false
Set the HttpOnly flag on the cookie (default: true)
hosts:
- host: "example.com"
captcha:
cookie:
http_only: true # <-- http_only
pending_ttlβ
string (parseable by time.ParseDuration)
TTL for pending captcha tokens (default: 30m)
hosts:
- host: "example.com"
captcha:
pending_ttl: "30m" # <-- pending_ttl
passed_ttlβ
string (parseable by time.ParseDuration)
TTL for passed captcha tokens (default: 24h)
hosts:
- host: "example.com"
captcha:
passed_ttl: "24h" # <-- passed_ttl
signing_keyβ
string (minimum 32 bytes)
Key used to sign captcha tokens (required when using captcha). Generate one with openssl rand -hex 32. If you run multiple SPOA instances serving the same domains, use the same signing_key everywhere so tokens validate consistently.
hosts:
- host: "example.com"
captcha:
signing_key: "<your-32-byte-minimum-secret-key>" # <-- signing_key
banβ
object
Ban remediation configuration for this host
hosts:
- host: "example.com"
ban:
contact_us_url: "https://example.com/support"
contact_us_urlβ
string
URL to display in ban templates for users to contact support this value is passed to an anchor tag href value
If you use a mailto: or tel: URL here, it will be visible in the rendered ban page and may be harvested by crawlers/spammers. Consider using a contact form URL instead, ideally hosted on a separate domain (or otherwise exempted) so it remains reachable while the main site is being challenged/blocked.
hosts:
- host: "example.com"
ban:
contact_us_url: "https://example.com/support" # <-- contact_us_url
log_levelβ
trace|debug|info|warn|error
Log level for this specific host (overrides the global log_level setting), useful when debugging a single host.
hosts:
- host: "example.com"
log_level: "info" # <-- host log_level
appsecβ
object
Host-level AppSec configuration (optional).
hosts:
- host: "example.com"
appsec:
always_send: false
always_sendβ
true|false
When false, AppSec evaluation is skipped if a higher-priority remediation already applies (for example ban or captcha).
hosts:
- host: "example.com"
appsec:
always_send: false # <-- always_send
urlβ
string
AppSec URL override for this host (defaults to global appsec_url).
hosts:
- host: "example.com"
appsec:
url: "http://127.0.0.1:7422" # <-- url
api_keyβ
string
AppSec API key override for this host (defaults to top-level api_key).
hosts:
- host: "example.com"
appsec:
api_key: "<appsec-api-key>" # <-- api_key
timeoutβ
string (parseable by time.ParseDuration)
AppSec request timeout for this host (default: 200ms).
hosts:
- host: "example.com"
appsec:
timeout: "200ms" # <-- timeout
hosts_dirβ
string
A directory containing .yaml files, each representing a host YAML struct. Each file should define all fields required by the host configuration structure.
hosts_dir: "/etc/crowdsec/bouncers/hosts.d"
asn_database_pathβ
string
Path to the GeoIP2 ASN database file (optional)
asn_database_path: "/var/lib/crowdsec/data/GeoLite2-ASN.mmdb"
city_database_pathβ
string
Path to the GeoIP2 City database file (optional)
city_database_path: "/var/lib/crowdsec/data/GeoLite2-City.mmdb"
prometheusβ
object
Prometheus metrics configuration
prometheus:
enabled: true
listen_addr: "127.0.0.1"
listen_port: "60601"
enabledβ
true|false
Enable Prometheus metrics endpoint
prometheus:
enabled: true # <-- enabled
listen_addrβ
string
Address to listen on for Prometheus metrics endpoint
prometheus:
listen_addr: "127.0.0.1" # <-- listen_addr
listen_portβ
string
Port to listen on for Prometheus metrics endpoint
prometheus:
listen_port: "60601" # <-- listen_port
pprofβ
object
Enable and expose Go pprof endpoints (debugging only).
pprof:
enabled: false
listen_addr: "127.0.0.1"
listen_port: "6060"
enabledβ
true|false
Enable the pprof endpoint (debugging only).
pprof:
enabled: true # <-- enabled
listen_addrβ
string
Address to listen on for pprof endpoint.
pprof:
listen_addr: "127.0.0.1" # <-- listen_addr
listen_portβ
string
Port to listen on for pprof endpoint.
pprof:
listen_port: "6060" # <-- listen_port
appsec_urlβ
string
Global CrowdSec AppSec URL (optional).
appsec_url: "http://127.0.0.1:7422"
appsec_timeoutβ
string (parseable by time.ParseDuration)
Global AppSec request timeout (default: 200ms).
appsec_timeout: "200ms"
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/cs-haproxy-spoa-bouncer.git
cd cs-haproxy-spoa-bouncer
make build
Configure the Bouncerβ
sudo mkdir -p /etc/crowdsec/bouncers/
sudo cp config/crowdsec-spoa-bouncer.yaml /etc/crowdsec/bouncers/
The configuration file is located at /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml:
log_mode: file
log_dir: /var/log/crowdsec-spoa/
log_level: info
compress_logs: true
log_max_size: 100
log_max_files: 3
log_max_age: 30
update_frequency: 10s
api_url: http://127.0.0.1:8080/
api_key: ${API_KEY}
insecure_skip_verify: false
# Optional (mTLS to LAPI)
#cert_path: /etc/ssl/certs/client.crt
#key_path: /etc/ssl/private/client.key
#ca_cert_path: /etc/ssl/certs/ca.crt
#retry_initial_connect: true
# Host configuration examples
hosts:
- host: "example.com"
captcha:
provider: "turnstile"
site_key: "<your-site-key>"
secret_key: "<your-secret-key>"
signing_key: "<your-32-byte-minimum-secret-key>"
pending_ttl: "30m"
passed_ttl: "24h"
cookie:
secure: "auto"
http_only: true
ban:
contact_us_url: "https://example.com/support"
appsec:
always_send: false
# url: "http://127.0.0.1:7422" # optional per-host override
# api_key: "<appsec-api-key>" # optional per-host override
# timeout: "200ms" # optional per-host override
log_level: "info"
- host: "*"
captcha:
fallback_remediation: "allow"
listen_tcp: 0.0.0.0:9000
listen_unix: /run/crowdsec-spoa/spoa.sock
prometheus:
enabled: false
listen_addr: 127.0.0.1
listen_port: "60601"
# Optional (AppSec)
#appsec_url: http://127.0.0.1:7422
#appsec_timeout: 200ms
# Optional (debug only)
#pprof:
# enabled: false
# listen_addr: 127.0.0.1
# listen_port: "6060"
Generate an API key:
sudo cscli bouncers add mybouncer
Then update the api_key field in the configuration file.
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
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The service runs as the crowdsec-spoa user. Ensure configuration files are readable by this user:
sudo chown root:crowdsec-spoa /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml
sudo chmod 640 /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml
If you have created .local variants of configuration files, apply the same permissions to those files as well.
Configure HAProxyβ
Follow the βHAProxy Configurationβ section above. Use send-spoe-group and the upstream /etc/haproxy/crowdsec.cfg (with spoe-groups). The upstream repository also ships full examples under config/.
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