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

This is the only HAProxy bouncer with AppSec support. It can forward HTTP requests to the AppSec Component for real-time WAF protection, virtual patching, and defense against known CVEs. See Enable AppSec (WAF) forwarding below.

For a full walkthrough, see the AppSec Quickstart 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 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).

Validate AppSec​

Test that malicious requests are blocked:

curl -I http://localhost/.env
# Expected: HTTP/1.1 403 Forbidden

Once AppSec is enabled, use cscli metrics show appsec to view processed vs. blocked requests and individual rule triggers. These metrics also appear in the CrowdSec Console after enrollment.

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