Skip to content

CrowdSec Bouncer

Protect your web service with CrowdSec and EU CAPTCHA together. The EU Captcha CrowdSec Bouncer is a standalone reverse proxy that intercepts requests, checks them against the CrowdSec Local API (LAPI), and — for IPs with a captcha decision — presents the EU CAPTCHA widget before allowing access.

IPs with a ban decision receive a 403 Forbidden response. All other traffic is proxied transparently to your backend.

How it works

Client → cs-eucaptcha-bouncer → Your application
              ↕
        CrowdSec LAPI
  1. The bouncer subscribes to the CrowdSec decision stream (/v1/decisions/stream).
  2. On each incoming request it looks up the client IP in its in-memory cache.
  3. Ban: returns 403 immediately.
  4. Captcha: redirects to /__captcha__, which serves the EU CAPTCHA widget. On completion, the token is verified server-side. A valid result sets an HMAC-signed cookie and redirects the client back to their original URL.
  5. No decision: proxies the request to the upstream.

Requirements

  • Go 1.22+ (to build from source) or a pre-built binary from the releases page
  • A running CrowdSec agent with an accessible Local API
  • An EU CAPTCHA account with a sitekey and secret

Installation

Pre-built binary

Download the binary for your platform from the releases page:

# Linux amd64 example
curl -L https://github.com/Myra-Security-GmbH/eu-captcha-crowdsec/releases/latest/download/cs-eucaptcha-bouncer-linux-amd64 \
  -o cs-eucaptcha-bouncer
chmod +x cs-eucaptcha-bouncer

Build from source

git clone https://github.com/Myra-Security-GmbH/eu-captcha-crowdsec.git
cd eu-captcha-crowdsec
make build
# produces ./cs-eucaptcha-bouncer

Configuration

Copy the example config and fill in your values:

cp config.yaml.example config.yaml
listen_addr: "0.0.0.0:8080"
upstream_url: "http://localhost:3000"

crowdsec:
  lapi_url: "http://localhost:8080"
  api_key: "<YOUR_CROWDSEC_API_KEY>"
  update_interval: "10s"

eu_captcha:
  sitekey: "EUCAPTCHA_SITE_KEY"
  secret:  "EUCAPTCHA_SECRET_KEY"

session:
  secret: "<32-byte hex string>"  # openssl rand -hex 32
  ttl: "1h"

# Optional: trust X-Forwarded-For from these upstream proxies
# trusted_proxies:
#   - "127.0.0.1/32"

Register the bouncer with CrowdSec

cscli bouncers add eu-captcha-bouncer
# Copy the printed API key into config.yaml → crowdsec.api_key

Running

./cs-eucaptcha-bouncer -config config.yaml

The bouncer logs to stdout using structured JSON (log/slog). Point your load balancer or DNS at listen_addr and ensure upstream_url points to your actual application.

Systemd unit (example)

[Unit]
Description=EU Captcha CrowdSec Bouncer
After=network.target crowdsec.service

[Service]
ExecStart=/usr/local/bin/cs-eucaptcha-bouncer -config /etc/eu-captcha-bouncer/config.yaml
Restart=on-failure
User=www-data

[Install]
WantedBy=multi-user.target

Docker

FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY . .
RUN go build -o cs-eucaptcha-bouncer ./cmd/cs-eucaptcha-bouncer

FROM alpine:3.19
COPY --from=builder /src/cs-eucaptcha-bouncer /usr/local/bin/
ENTRYPOINT ["cs-eucaptcha-bouncer", "-config", "/etc/bouncer/config.yaml"]

Configuration reference

Key Default Description
listen_addr 0.0.0.0:8080 Address the bouncer listens on
upstream_url (required) Backend to proxy to
crowdsec.lapi_url http://localhost:8080 CrowdSec LAPI base URL
crowdsec.api_key (required) Bouncer API key from cscli bouncers add
crowdsec.update_interval 10s Decision stream poll interval
eu_captcha.sitekey (required) EU CAPTCHA public sitekey
eu_captcha.secret (required) EU CAPTCHA private secret
eu_captcha.verify_url https://api.eu-captcha.eu/v1/verify Verification endpoint
session.secret (required) HMAC signing key (openssl rand -hex 32)
session.cookie_name __eucaptcha_pass Session cookie name
session.ttl 1h Validity period of a passed challenge
trusted_proxies (empty) CIDR list of upstream proxies to trust for X-Forwarded-For

Reserved paths

The bouncer reserves two URL paths on the proxied domain:

Path Purpose
/__captcha__ Serves the EU CAPTCHA challenge page
/__captcha__/verify Accepts the completed token via POST

Do not use these paths in your application.

Server-side verification

The bouncer calls the EU CAPTCHA API on every completed challenge. For details on the verification response, see the Server-Side Verification guide. Note that responses with "train": true are treated as failures — the bouncer will not grant access for training samples.

See also

  • Caddy Middleware — gate any Caddy route behind EU CAPTCHA without CrowdSec
  • Traefik Plugin — EU CAPTCHA with CrowdSec as a Traefik middleware; no separate binary required

Source

github.com/Myra-Security-GmbH/eu-captcha-crowdsec