Skip to main content

CrowdSec

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

AppSecUnsupported
ModeStream only
MetricsSupported
MTLSUnsupported
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 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.

sudo apt 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/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-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
## src-ip is included as fallback in case crowdsec-ip message didn't fire
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=src src-port=src_port
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/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
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-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.

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.

Most CDNs add an X-Real-IP or X-Forwarded-For header to the request to pass the original client IP. Ensure your CDN is configured to add this header, and adjust the examples below if your CDN uses a different header name.

Configuration Changes​

When HAProxy is behind a CDN, modify your /etc/haproxy/crowdsec.cfg to:

  1. Use only the crowdsec-http message (the crowdsec-ip message will capture the CDN edge IP, which is not useful)
  2. Extract the real client IP from the CDN header using req.hdr_ip() to convert it to HAProxy's IP type
  3. 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-http is used. The crowdsec-ip message would run at on-client-session and 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 the X-Real-IP header 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-IP for Cloudflare), adjust the header name accordingly. For Cloudflare specifically, use req.hdr_ip(cf-connecting-ip).

Since your SPOA bouncer now relies on the X-Real-IP header to determine the client IP, it is critical to ensure that only your trusted upstream CDN proxy can connect to your HAProxy server.

If you do not properly firewall your HAProxy port, an attacker could connect directly and spoof the X-Real-IP header, bypassing your security rules.

Ensure your firewall is configured to only allow connections to your HAProxy port (typically 80/443) from your upstream CDN provider's IP ranges. Always verify your CDN provider's current IP ranges and keep your firewall rules up to date.

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 ProviderHeader NameHAProxy Function
Generic / Most CDNsX-Real-IPreq.hdr_ip(x-real-ip)
CloudflareCF-Connecting-IPreq.hdr_ip(cf-connecting-ip)
AWS CloudFrontCloudFront-Viewer-Addressreq.hdr_ip(cloudfront-viewer-address)
AkamaiTrue-Client-IPreq.hdr_ip(true-client-ip)
Azure CDNX-Forwarded-Forreq.hdr_ip(x-forwarded-for)

If your CDN uses X-Forwarded-For with multiple IPs (comma-separated), you'll need to extract the correct IP. For example:

src-ip=req.hdr_ip(x-forwarded-for,1)

This tells HAProxy to use the first IP from the comma-separated list. If your CDN appends IPs from right to left (instead of left to right), you can use -1 to extract the rightmost IP:

src-ip=req.hdr_ip(x-forwarded-for,-1)

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​

You can find a default configuration hosted on the Github Repository this is provided with the installation package.

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_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.

log_level​

trace | debug | info | warn | error

Log level (default: info)

log_compression​

true | false

Compress log files on rotation (default: true)

log_max_size​

int (in MB)

Max size of log files before rotation (default: 500)

log_max_backups​

int

How many backup log files to keep before deletion (can happen before log_max_age is reached) (default: 3)

log_max_age​

int (in days)

Max age of backup files before deletion (can happen before log_max_backups is reached) (default: 30)

update_frequency​

string (That is parseable by time.ParseDuration)

Frequency to contact the API for new/deleted decisions (default: 10s)

api_url​

string

URL of the local API EG: http://127.0.0.1:8080

api_key​

string

API key to authenticate with the local API

insecure_skip_verify​

true | false

Skip verification of the API certificate, typical for self-signed certificates

listen_tcp​

string

TCP address and port to listen on for SPOE connections. Format: ip:port or :port

At least one of listen_tcp or listen_unix must be configured.

listen_unix​

string

Unix socket path to listen on for SPOE connections

At least one of listen_tcp or listen_unix must be configured.

hosts​

[]object

List of host configurations for domain-specific settings

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.

captcha​

object

CAPTCHA configuration for this host

provider​

hcaptcha | recaptcha | turnstile

CAPTCHA provider to use

site_key​

string

CAPTCHA site key

secret_key​

string

CAPTCHA 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)

timeout​

int (in seconds)

HTTP client timeout in seconds, maximum 300 (default: 5)

object

Cookie generation configuration

sign_cookies​

true | false

Sign the cookie value (default: true)

secure​

auto | always | never

Set the secure flag on the cookie. auto relies on the ssl_fc flag from HAProxy (default: auto)

http_only​

true | false

Set the HttpOnly flag on the cookie (default: true)

secret​

string

Secret used for signed/encrypted cookies (default: uses the secret key of the captcha provider)

session_idle_timeout​

string (That is parseable by time.ParseDuration)

Session idle timeout duration (default: 1h)

session_max_time​

string (That is parseable by time.ParseDuration)

Maximum session lifetime duration (default: 12h)

session_garbage_seconds​

int (in seconds)

Interval in seconds for garbage collection of expired sessions (default: 60)

ban​

object

Ban remediation configuration for this host

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

log_level​

trace | debug | info | warn | error

Log level for this specific host (overrides the global log_level setting)

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.

asn_database_path​

string

Path to the GeoIP2 ASN database file (optional)

city_database_path​

string

Path to the GeoIP2 City database file (optional)

prometheus​

object

Prometheus metrics configuration

enabled​

true | false

Enable Prometheus metrics endpoint

listen_addr​

string

Address to listen on for Prometheus metrics endpoint

listen_port​

int

Port to listen on for Prometheus metrics endpoint

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/

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
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

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

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​

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-haproxy-spoa-bouncer/html/ban.html
setenv CROWDSEC_CAPTCHA_TEMPLATE_PATH /var/lib/crowdsec-haproxy-spoa-bouncer/html/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=req.body url=url ssl=ssl_fc src-ip=src src-port=src_port
event on-frontend-http-request
Add SPOE Backend​
backend crowdsec-spoa
mode tcp
balance roundrobin
server s1 127.0.0.1:9000

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