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/bouncer/crowdsec-spoa-bouncer.yaml should already be in a working state, and can skip this section and begin with HAProxy Configuration.

If your CrowdSec Engine is installed on an other server, you'll have to update the /etc/crowdsec/bouncer/crowdsec-spoa-bouncer.yaml file.

HAProxy Configuration​

HAProxy requires two configuration files for integration with the bouncer. The primary file is /etc/haproxy/haproxy.cfg, which must be modified to enable communication with the SPOE engineβ€”our documentation will guide you through this. The second file is /etc/haproxy/crowdsec.cfg, which contains the SPOE agent configuration. This file is automatically installed along with the bouncer package on the condition that /etc/haproxy exists.

If you are using packages, you will find the haproxy configuration snippets in /usr/share/doc/crowdsec-haproxy-spoa-bouncer/examples.

SPOE Filter​

Add a SPOE agent configuration to /etc/haproxy/crowdsec.cfg:

/etc/haproxy/crowdsec.cfg
[crowdsec]
spoe-agent crowdsec-agent
messages crowdsec-ip crowdsec-http

option var-prefix crowdsec
option set-on-error error
timeout hello 100ms
timeout idle 30s
timeout processing 500ms
use-backend crowdsec-spoa
log global

## This message is used to customise the remediation from crowdsec-ip based on the host header
spoe-message crowdsec-http
args remediation=var(txn.crowdsec.remediation) crowdsec_captcha_cookie=req.cook(crowdsec_captcha_cookie) id=unique-id host=hdr(Host) method=method path=path query=query version=req.ver headers=req.hdrs body=req.body url=url ssl=ssl_fc
event on-frontend-http-request

## This message should be the first to trigger in the chain
spoe-message crowdsec-ip
args id=unique-id src-ip=src src-port=src_port
event on-client-session

If you installed the haproxy spoe bouncer through package, you will find this configuration file in /usr/share/docs/crowdsec-haproxy-spoa-bouncer/examples

This crowdsec spoe agent configuration is then referenced in the main haproxy configuration file /etc/haproxy/haproxy.cfg and may be added at the bottom of the haproxy configuration file.

/etc/haproxy/haproxy.cfg
[...]

frontend http-in
bind *:80
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg
http-request set-header X-CrowdSec-Remediation %[var(txn.crowdsec.remediation)]

## Handle 302 redirect for successful captcha validation (native HAProxy redirect)
http-request redirect code 302 location %[var(txn.crowdsec.redirect)] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }

## Call lua script only for ban and captcha remediations (performance optimization)
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "captcha" }
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "ban" }

## Handle captcha cookie management via HAProxy (new approach)
## Set captcha cookie when SPOA provides captcha_status (pending or valid)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_status) -m found } { var(txn.crowdsec.captcha_cookie) -m found }
## Clear captcha cookie when cookie exists but no captcha_status (Allow decision)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_cookie) -m found } !{ var(txn.crowdsec.captcha_status) -m found }

use_backend <whatever>

backend crowdsec-spoa
mode tcp
server s1 127.0.0.1:9000

In the global section of your haproxy.cfg, lua path configuration is also mandatory:

global
[...]
lua-load /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/crowdsec.lua

An example that includes this snippet can also be found in /usr/share/docs/crowdsec-haproxy-spoa-bouncer/examples/haproxy.cfg.

Specific features​

To enable CAPTCHA for a domain:​

hosts:
- host: "example.com"
captcha:
site_key: "<your-site-key>"
secret_key: "<your-secret-key>"
provider: "hcaptcha"

The following captcha providers are supported:

hcaptcha
recaptcha
turnstile

HAProxy Behind a CDN​

When HAProxy is deployed behind an upstream Content Delivery Network (CDN), the source IP seen by HAProxy will be the CDN's edge server IP, not the real client IP. To properly evaluate and apply security rules based on the actual client IP, you need to configure the SPOA to extract the real IP from the CDN-provided header.

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.

Admin Socket​

You can query the bouncer runtime state using the admin socket:

socat - UNIX-CONNECT:/run/crowdsec-spoa-admin.sock

Commands:

    get hosts
get host <host> session <uuid> <key>
set host <host> session <uuid> <key> <value>
get ip <ip>
val host <host> cookie <cookie>
val host <host> captcha <response>

Manual installation and advanced configuration​

We strongly encourage the use of our packages.

Compile the Binary​

This requires a whole working golang installation.

git clone https://github.com/crowdsecurity/crowdsec-spoa-bouncer.git
cd crowdsec-spoa-bouncer
make build

Configure the Bouncer​

sudo mkdir -p /etc/crowdsec/bouncers/
sudo cp config/crowdsec-spoa-bouncer.yaml /etc/crowdsec/bouncers/

You can always edit the configuration file at /etc/crowdsec/bouncer/crowdsec-spoa-bouncer.yaml:

/etc/crowdsec/bouncer/crowdsec-spoa-bouncer.yaml
log_mode: file
log_dir: /var/log/
log_level: info
log_compression: true
log_max_size: 100
log_max_backups: 3
log_max_age: 30

update_frequency: 10s
api_url: http://127.0.0.1:8080/
api_key: ${API_KEY}
insecure_skip_verify: false

workers:
- name: spoa1
listen_addr: 0.0.0.0:9000
listen_socket: /run/crowdsec-spoa/spoa-1.sock

worker_user: crowdsec-spoa
worker_group: crowdsec-spoa

asn_database_path: /var/lib/crowdsec/data/GeoLite2-ASN.mmdb
city_database_path: /var/lib/crowdsec/data/GeoLite2-City.mmdb

admin_socket: /run/crowdsec-spoa-admin.sock

prometheus:
enabled: true
listen_addr: 127.0.0.1
listen_port: 60601

You can get a workable configuration by using the yaml above and getting and api key by:

sudo cscli bouncers add mybouncer
API key for 'bouncertest':

JdVa7DKBM35gPDAR014pH/55l38fxLGt02NPPnZgLQI

Please keep this key since you will not be able to retrieve it!
  • Paste the key into:
    api_key: your-generated-key

In the /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml file the following keys are of some importance:

  • Set your LAPI URL to point to your CrowdSec LAPI instance:
    api_url: http://127.0.0.1:8080/

You can check that the bouncer is correctly installed with cscli:

❯ sudo cscli bouncers list
──────────────────────────────────────────────────────────────────────────────────────────
Name IP Address Valid Last API pull Type
──────────────────────────────────────────────────────────────────────────────────────────
cs-spoa-bouncer-1752052534 127.0.0.1 βœ”οΈ crowdsec-spoa-bouncer
──────────────────────────────────────────────────────────────────────────────────────────
❯ sudo cscli bouncers inspect cs-spoa-bouncer-1752052534
──────────────────────────────────────────────────────────────────────────────────────────
Bouncer: cs-spoa-bouncer-1752052534
──────────────────────────────────────────────────────────────────────────────────────────
Created At 2025-07-09 09:15:34.685444393 +0000 UTC
Last Update 2025-07-09 12:42:18.92023029 +0000 UTC
Revoked? false
IP Address 127.0.0.1
Type crowdsec-spoa-bouncer
Version v0.0.3-beta29-rpm-pragmatic-arm64-db7065289a0f5ce1c92f34807c9a98b23c07dc90
Last Pull
Auth type api-key
OS ?
Auto Created false
──────────────────────────────────────────────────────────────────────────────────────────

Create runtime socket directory and crowdsec-spoa user:

sudo
sudo mkdir -p /run/crowdsec-spoa
sudo chown crowdsec-spoa:crowdsec-spoa /run/crowdsec-spoa

Configure HAProxy​

Lua Integration & Environment Variables​

In the global section of your haproxy.cfg, configure Lua paths and template environment:

global
lua-prepend-path /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/?.lua
lua-load /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/crowdsec.lua

setenv CROWDSEC_BAN_TEMPLATE_PATH /var/lib/crowdsec/lua/haproxy/templates/ban.html
setenv CROWDSEC_CAPTCHA_TEMPLATE_PATH /var/lib/crowdsec/lua/haproxy/templates/captcha.html

These variables are used by the Lua module to render proper HTML responses for banned or captcha-validated users.

Add SPOE Filter in frontend​
frontend test
mode http
bind *:9090

filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg

http-request set-header X-CrowdSec-Remediation %[var(txn.crowdsec.remediation)] if { var(txn.crowdsec.remediation) -m found }
http-request set-header X-CrowdSec-IsoCode %[var(txn.crowdsec.isocode)] if { var(txn.crowdsec.isocode) -m found }

## Handle 302 redirect for successful captcha validation (native HAProxy redirect)
http-request redirect code 302 location %[var(txn.crowdsec.redirect)] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }

## Call lua script only for ban and captcha remediations (performance optimization)
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "captcha" }
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "ban" }

## Handle captcha cookie management via HAProxy (new approach)
## Set captcha cookie when SPOA provides captcha_status (pending or valid)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_status) -m found } { var(txn.crowdsec.captcha_cookie) -m found }
## Clear captcha cookie when cookie exists but no captcha_status (Allow decision)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_cookie) -m found } !{ var(txn.crowdsec.captcha_status) -m found }

use_backend test_backend
Create SPOE Config​

Create /etc/haproxy/crowdsec.cfg:

/etc/haproxy/crowdsec.cfg
spoe-agent crowdsec-agent
messages crowdsec-ip crowdsec-http
option var-prefix crowdsec
option set-on-error error
timeout hello 100ms
timeout idle 30s
timeout processing 500ms
use-backend crowdsec-spoa

spoe-message crowdsec-ip
args id=unique-id src-ip=src src-port=src_port
event on-client-session

spoe-message crowdsec-http
args remediation=var(txn.crowdsec.remediation) crowdsec_captcha_cookie=req.cook(crowdsec_captcha_cookie) id=unique-id host=hdr(Host) method=method path=path query=query version=req.ver headers=req.hdrs body=re
q.body url=url ssl=ssl_fc
event on-frontend-http-request
Add SPOE Backend​
backend crowdsec-spoa
mode tcp
balance roundrobin
server s1 127.0.0.1:9000

Modify HAProxy systemd Unit (Optional)​

Edit /etc/systemd/system/haproxy.service and add:

[Service]
Environment=CROWDSEC_BAN_TEMPLATE_PATH=/var/lib/crowdsec/lua/haproxy/templates/ban.html
Environment=CROWDSEC_CAPTCHA_TEMPLATE_PATH=/var/lib/crowdsec/lua/haproxy/templates/captcha.html

Then reload systemd:

sudo systemctl daemon-reexec
sudo systemctl daemon-reload

Start the Bouncer​

Run Directly

sudo ./crowdsec-spoa-bouncer -c /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml

Or Run as a Systemd Service

sudo cp config/crowdsec-spoa-bouncer.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now crowdsec-spoa-bouncer