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
- The bouncer subscribes to the CrowdSec decision stream (
/v1/decisions/stream). - On each incoming request it looks up the client IP in its in-memory cache.
- Ban: returns 403 immediately.
- 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. - 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