Skip to main content

CrowdSec

πŸ“š Documentation πŸ’  Hub πŸ’¬ Discourse

AppSecSupported
ModeStream only
MetricsSupported
MTLSSupported
PrometheusSupported

A remediation component for HAProxy

Beta Remediation Component, please report any issues on GitHub

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.

sudo apt 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 ProviderHeader NameHAProxy Function
Generic / Most CDNsX-Real-IPhdr_ip(X-Real-IP)
CloudflareCF-Connecting-IPhdr_ip(CF-Connecting-IP)
AWS CloudFrontCloudFront-Viewer-Addresshdr_ip(CloudFront-Viewer-Address)
AkamaiTrue-Client-IPhdr_ip(True-Client-IP)
Azure CDNX-Forwarded-Forhdr_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

Enable AppSec (WAF) forwarding​

The SPOA bouncer can forward requests to CrowdSec AppSec for WAF evaluation.

Prerequisites:

Enable it in the bouncer configuration:

# 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-body group when the body is available (see the body_within_limit + send-spoe-group example 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:

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)

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