Skip to main content
HAProxy SPOA logo
HAProxy SPOA

Go-based SPOA bouncer for HAProxy with WAF (AppSec) support — the recommended HAProxy integration.

  • Stream
  • WAF / AppSec
  • Captcha
  • ×Bot Detection
  • Remediation Metrics
  • mTLS
  • Prometheus
  • GitHub·Hub

Protect HAProxy with IP Blocking & WAF

Enable the WAF for optimal protection

This is the only HAProxy bouncer with WAF (AppSec) support. After installing the bouncer, enable the WAF (AppSec) Component for real-time WAF protection, virtual patching, and defense against known CVEs.

Follow the dedicated WAF Quickstart for HAProxy — it picks up right where this page ends.

What it does

The cs-haproxy-spoa-bouncer allows CrowdSec to enforce blocking, CAPTCHA, or allow actions directly within HAProxy using the SPOE protocol.

This remediation component is meant to obsolete the old lua-based haproxy bouncer.

It supports IP-based decisions, CAPTCHA challenges, GeoIP-based headers, and integrates cleanly with CrowdSec’s LAPI using the stream bouncer protocol.

Supported features:

  • Stream mode (pull LAPI decisions periodically)
  • mTLS to LAPI (via cert_path / key_path / ca_cert_path)
  • IP / range / country decisions
  • Ban remediation (custom HTML / redirects)
  • CAPTCHA remediation (hCaptcha / reCAPTCHA / Turnstile)
  • GeoIP headers (ASN / Country)
  • WAF (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.

SH
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
TEXT
[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

HAProxy 2.2+ can serve ban and captcha pages natively using http-request return with lf-file (log-format file). No Lua is required.

HAPROXY
[...]

frontend http-in
bind *:80
option http-buffer-request

unique-id-format %[uuid()]
unique-id-header X-Unique-ID
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg

# Body size limit: stay safely under SPOE frame limit (~50 KB)
acl body_within_limit req.body_size -m int le 51200

# HTML-capable client detection
acl render_html req.hdr_cnt(Accept) eq 0
acl render_html req.hdr(Accept) -m sub text/html
acl render_html req.hdr(Accept) -m sub */*
acl html_rejected req.hdr(Accept) -m reg "text/html;q=0"

acl has_contact_url var(txn.crowdsec.contact_us_url) -m found
acl empty_contact_url var(txn.crowdsec.contact_us_url) -m str ""

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)] 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 }

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

## Serve remediation pages with native HAProxy lf-file directives (no Lua needed)
http-request return status 200 content-type "text/html; charset=utf-8" hdr Cache-Control "no-cache, no-store" lf-file /var/lib/crowdsec-haproxy-spoa-bouncer/html/captcha.html if { var(txn.crowdsec.remediation) -m str "captcha" } render_html !html_rejected
http-request return status 403 content-type "text/html; charset=utf-8" hdr Cache-Control "no-cache, no-store" lf-file /var/lib/crowdsec-haproxy-spoa-bouncer/html/ban-with-contact.html if { var(txn.crowdsec.remediation) -m str "ban" } render_html !html_rejected has_contact_url !empty_contact_url
http-request return status 403 content-type "text/html; charset=utf-8" hdr Cache-Control "no-cache, no-store" lf-file /var/lib/crowdsec-haproxy-spoa-bouncer/html/ban.html if { var(txn.crowdsec.remediation) -m str "ban" } render_html !html_rejected !has_contact_url
http-request return status 403 content-type "text/html; charset=utf-8" hdr Cache-Control "no-cache, no-store" lf-file /var/lib/crowdsec-haproxy-spoa-bouncer/html/ban.html if { var(txn.crowdsec.remediation) -m str "ban" } render_html !html_rejected empty_contact_url
## Plain-text fallback for non-HTML clients
http-request return status 200 content-type text/plain hdr Cache-Control "no-cache, no-store" string "Captcha required\n" if { var(txn.crowdsec.remediation) -m str "captcha" } !render_html
http-request return status 200 content-type text/plain hdr Cache-Control "no-cache, no-store" string "Captcha required\n" if { var(txn.crowdsec.remediation) -m str "captcha" } html_rejected
http-request return status 403 content-type text/plain hdr Cache-Control "no-cache, no-store" string "Forbidden\n" if { var(txn.crowdsec.remediation) -m str "ban" } !render_html
http-request return status 403 content-type text/plain hdr Cache-Control "no-cache, no-store" string "Forbidden\n" if { var(txn.crowdsec.remediation) -m str "ban" } html_rejected

## Captcha cookie management
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 }
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 <your-backend>

backend crowdsec-spoa
mode tcp
timeout connect 2s
timeout server 60s
server spoa 127.0.0.1:9000

The complete example (with global and defaults) is also available at /usr/share/doc/crowdsec-haproxy-spoa-bouncer/examples/haproxy.cfg.

Optional: Lua-based template rendering

Lua support is optional. If you need dynamic template rendering beyond what lf-file provides (for example, injecting runtime variables not available as HAProxy log-format fetch methods), you can load the bundled Lua scripts instead.

Add to the global section of haproxy.cfg:

HAPROXY
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/lua/ban.html
setenv CROWDSEC_CAPTCHA_TEMPLATE_PATH /var/lib/crowdsec-haproxy-spoa-bouncer/html/lua/captcha.html

Then replace the http-request return block in the frontend with:

HAPROXY
    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" }

The Lua example config is at /usr/share/doc/crowdsec-haproxy-spoa-bouncer/examples/haproxy-lua.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:

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

Firewall rules for trusted proxies

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.

Container

The container image runs the SPOA bouncer (it does not bundle HAProxy): crowdsecurity/spoa-bouncer.

warning

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:

SH
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

YAML
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/
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
ports:
- "80:80"
- "443:443"
depends_on:
- crowdsec-spoa-bouncer
networks:
- crowdsec

volumes:
crowdsec-db:
crowdsec-config:
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:

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

The HTML templates (/var/lib/crowdsec-haproxy-spoa-bouncer/html/) are needed by the non-Lua lf-file HAProxy directives. If you run HAProxy without the package, mount or copy the html/ directory from the image into your HAProxy environment. The Lua scripts (/usr/lib/crowdsec-haproxy-spoa-bouncer/lua/) are only required if you choose the optional Lua rendering approach.

For all container options and environment variables, see: https://github.com/crowdsecurity/cs-haproxy-spoa-bouncer/blob/main/docker/README.md

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

If your CDN uses X-Forwarded-For with multiple IPs (comma-separated), you may need to select the right one:

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

HAPROXY
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

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

TEXT
hcaptcha
recaptcha
turnstile
Provider registration

AppSec (WAF) forwarding — configuration reference

Turning this on

To enable the WAF end-to-end, follow the AppSec Quickstart for HAProxy — it picks up after the bouncer install and walks through collections, acquisition, and enabling forwarding.

Relevant bouncer keys in /etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml:

YAML
# Global AppSec URL (optional)
appsec_url: http://127.0.0.1:7422
appsec_timeout: 200ms

hosts:
- host: "*"
appsec:
always_send: false
# url: http://custom-appsec:7422 # optional per-host override
# api_key: custom-key # optional per-host override

HAProxy requirements when using AppSec (and/or captcha):

  • Enable request buffering: option http-buffer-request
  • Increase HAProxy buffer size (max 64KB): tune.bufsize 65536
  • Use the crowdsec-http-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:

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

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

YAML
log_dir: "/var/log/crowdsec-spoa/"

log_level

trace | debug | info | warn | error

Log level (default: info)

YAML
log_level: "info"

compress_logs

true | false

Compress log files on rotation (default: true)

YAML
compress_logs: true

log_max_size

int (in MB)

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

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

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

YAML
log_max_age: 30
info

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)

YAML
update_frequency: "10s"

api_url

string

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

YAML
api_url: "https://lapi.example.com:8080/"

api_key

string

API key to authenticate with the local API

YAML
api_key: "<your-lapi-api-key>"

insecure_skip_verify

true | false

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

YAML
insecure_skip_verify: false

cert_path

string

Client certificate path for mTLS to LAPI.

YAML
cert_path: "/etc/ssl/certs/client.crt"

key_path

string

Client private key path for mTLS to LAPI.

YAML
key_path: "/etc/ssl/private/client.key"

ca_cert_path

string

CA certificate path for validating the LAPI certificate (mTLS / custom CAs).

YAML
ca_cert_path: "/etc/ssl/certs/ca.crt"

retry_initial_connect

true | false

Retry connecting to LAPI on startup instead of failing fast.

YAML
retry_initial_connect: true

scopes

[]string

Only pull decisions matching these scopes (for example ip, range, country).

YAML
scopes: ["ip", "range", "country"]

scenarios_containing

[]string

Only pull decisions whose scenario contains one of these strings.

YAML
scenarios_containing: ["crowdsecurity/"]

scenarios_not_containing

[]string

Do not pull decisions whose scenario contains one of these strings.

YAML
scenarios_not_containing: ["whitelist"]

origins

[]string

Only pull decisions from these origins.

YAML
origins: ["crowdsecurity", "lists"]

listen_tcp

string

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

YAML
listen_tcp: "0.0.0.0:9000"
info

At least one of listen_tcp or listen_unix must be configured.

listen_unix

string

Unix socket path to listen on for SPOE connections

YAML
listen_unix: "/run/crowdsec-spoa/spoa.sock"
info

At least one of listen_tcp or listen_unix must be configured.

hosts

[]object

List of host configurations for domain-specific settings

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

YAML
hosts:
- host: "*.example.com" # <-- host pattern

captcha

object

CAPTCHA configuration for this host

YAML
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

YAML
hosts:
- host: "example.com"
captcha:
provider: "turnstile" # <-- provider
site_key

string

CAPTCHA site key

YAML
hosts:
- host: "example.com"
captcha:
site_key: "<your-site-key>" # <-- site_key
secret_key

string

CAPTCHA secret key

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

YAML
hosts:
- host: "*"
captcha:
fallback_remediation: "allow" # <-- fallback_remediation
timeout

int (in seconds)

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

YAML
hosts:
- host: "example.com"
captcha:
timeout: 5 # <-- timeout (seconds)

object

Cookie generation configuration

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

YAML
hosts:
- host: "example.com"
captcha:
cookie:
secure: "auto" # <-- secure
http_only

true | false

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

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

YAML
hosts:
- host: "example.com"
captcha:
pending_ttl: "30m" # <-- pending_ttl
passed_ttl

string (parseable by time.ParseDuration)

TTL for passed captcha tokens (default: 24h)

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

YAML
hosts:
- host: "example.com"
captcha:
signing_key: "<your-32-byte-minimum-secret-key>" # <-- signing_key

ban

object

Ban remediation configuration for this host

YAML
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

warning

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.

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

YAML
hosts:
- host: "example.com"
log_level: "info" # <-- host log_level

appsec

object

Host-level AppSec configuration (optional).

YAML
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).

YAML
hosts:
- host: "example.com"
appsec:
always_send: false # <-- always_send
url

string

AppSec URL override for this host (defaults to global appsec_url).

YAML
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).

YAML
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).

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

YAML
hosts_dir: "/etc/crowdsec/bouncers/hosts.d"

asn_database_path

string

Path to the GeoIP2 ASN database file (optional)

YAML
asn_database_path: "/var/lib/crowdsec/data/GeoLite2-ASN.mmdb"

city_database_path

string

Path to the GeoIP2 City database file (optional)

YAML
city_database_path: "/var/lib/crowdsec/data/GeoLite2-City.mmdb"

prometheus

object

Prometheus metrics configuration

YAML
prometheus:
enabled: true
listen_addr: "127.0.0.1"
listen_port: "60601"

enabled

true | false

Enable Prometheus metrics endpoint

YAML
prometheus:
enabled: true # <-- enabled

listen_addr

string

Address to listen on for Prometheus metrics endpoint

YAML
prometheus:
listen_addr: "127.0.0.1" # <-- listen_addr

listen_port

string

Port to listen on for Prometheus metrics endpoint

YAML
prometheus:
listen_port: "60601" # <-- listen_port

pprof

object

Enable and expose Go pprof endpoints (debugging only).

YAML
pprof:
enabled: false
listen_addr: "127.0.0.1"
listen_port: "6060"

enabled

true | false

Enable the pprof endpoint (debugging only).

YAML
pprof:
enabled: true # <-- enabled

listen_addr

string

Address to listen on for pprof endpoint.

YAML
pprof:
listen_addr: "127.0.0.1" # <-- listen_addr

listen_port

string

Port to listen on for pprof endpoint.

YAML
pprof:
listen_port: "6060" # <-- listen_port

appsec_url

string

Global CrowdSec AppSec URL (optional).

YAML
appsec_url: "http://127.0.0.1:7422"

appsec_timeout

string (parseable by time.ParseDuration)

Global AppSec request timeout (default: 200ms).

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

SH
git clone https://github.com/crowdsecurity/cs-haproxy-spoa-bouncer.git
cd cs-haproxy-spoa-bouncer
make build

Configure the Bouncer

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

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:

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

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

File Permissions

The service runs as the crowdsec-spoa user. Ensure configuration files are readable by this user:

SH
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

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

Or Run as a Systemd Service

SH
sudo cp config/crowdsec-spoa-bouncer.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now crowdsec-spoa-bouncer
CrowdSec Docs
We use cookies

This site uses cookies to help us improve your experience. You can accept or decline below.