Caddy Middleware
The eu-captcha-caddy module is a Caddy HTTP handler (http.handlers.eu_captcha) that gates any route behind EU CAPTCHA. Every visitor reaching the middleware must pass the challenge before Caddy forwards the request to the upstream. A signed session cookie tracks visitors who have already passed, so they are not re-challenged until the grace period expires.
Unlike the CrowdSec-based integrations, this module requires no external decision engine — it challenges every visitor on first access, regardless of their IP reputation.
How it works
Client → Caddy (eu_captcha handler) → Your upstream
- On each request the handler checks for a valid signed session cookie.
- Cookie valid — the request is forwarded to the next handler unchanged.
- No cookie / expired / wrong IP — the visitor is redirected to the built-in challenge page.
- The visitor completes the EU CAPTCHA widget; the browser posts the token to the verify endpoint.
- The token is verified server-side via the EU CAPTCHA API. On success, a signed cookie is set and the visitor is redirected to their original URL.
At startup the handler calls /verify-credentials in a background goroutine and logs whether the sitekey and secret are valid.
Requirements
- Caddy v2.9+ (custom binary built with xcaddy)
- An EU CAPTCHA account with a sitekey and secret
- A random 32-byte value for signing session cookies
Installation
The module is not included in the standard Caddy binary. Build a custom binary with xcaddy:
xcaddy build --with github.com/Myra-Security-GmbH/eu-captcha-caddy
The resulting caddy binary includes the eu_captcha directive.
Quick start
example.com {
route {
eu_captcha {
sitekey {env.EUCAPTCHA_SITEKEY}
secret {env.EUCAPTCHA_SECRET}
cookie_secret {env.EUCAPTCHA_COOKIE_SECRET}
}
reverse_proxy localhost:3000
}
}
Set the three environment variables before starting Caddy:
export EUCAPTCHA_SITEKEY=your-public-sitekey
export EUCAPTCHA_SECRET=your-secret-key
export EUCAPTCHA_COOKIE_SECRET=$(openssl rand -hex 32)
cookie_secret is the HMAC signing key for session cookies. Generate it once and store it securely — changing it invalidates all active sessions.
eu_captchamust be placed inside arouteblock so Caddy evaluates it beforereverse_proxy. Withoutroute, Caddy's handler ordering may applyreverse_proxyfirst.
Caddyfile reference
eu_captcha {
sitekey <public-sitekey>
secret <secret-key>
cookie_secret <hmac-signing-key>
grace_period <duration>
cookie_name <name>
challenge_path <path>
verify_url <url>
credentials_check_url <url>
}
| Subdirective | Required | Default | Description |
|---|---|---|---|
sitekey |
✓ | — | Public sitekey for the client-side widget |
secret |
✓ | — | Secret key for server-side token verification — keep server-side only |
cookie_secret |
✓ | — | HMAC signing key for session cookies. Use at least 32 random bytes (openssl rand -hex 32). |
grace_period |
1h |
How long a solved challenge is valid. Accepts Go duration strings: 30m, 2h, 24h. |
|
cookie_name |
__eucaptcha |
Name of the session cookie set on the visitor's browser | |
challenge_path |
/__captcha__ |
Path prefix for the built-in challenge page and verify endpoint | |
verify_url |
https://api.eu-captcha.eu/v1/verify |
EU CAPTCHA token verification endpoint | |
credentials_check_url |
https://api.eu-captcha.eu/v1/verify-credentials |
Startup credentials check endpoint |
JSON configuration
If you configure Caddy via its JSON API, use http.handlers.eu_captcha as the handler name:
{
"handler": "eu_captcha",
"sitekey": "YOUR_SITEKEY",
"secret": "YOUR_SECRET",
"cookie_secret": "YOUR_COOKIE_SECRET",
"grace_period": "1h",
"cookie_name": "__eucaptcha",
"challenge_path": "/__captcha__"
}
Reserved paths
The handler reserves two sub-paths under challenge_path:
| Path | Method | Purpose |
|---|---|---|
/__captcha__ |
GET | Serves the built-in EU CAPTCHA challenge page |
/__captcha__/verify |
POST | Receives the completed token and verifies it server-side |
These paths are handled internally and never forwarded to the upstream. Do not use them in your application.
Session cookie
On a successful verification the handler sets a cookie with:
- Name: the configured
cookie_name(default__eucaptcha) - Value: HMAC-SHA256 signed payload containing the visitor IP and expiry timestamp
- Expiry: the configured
grace_period(default1h) - Flags:
HttpOnly,SameSite=Strict
The visitor's IP is bound into the cookie so it cannot be replayed from a different address.
Protecting specific routes
Apply the handler only to routes that need protection by using separate route blocks:
example.com {
# Protected — challenge required
route /app/* {
eu_captcha {
sitekey {env.EUCAPTCHA_SITEKEY}
secret {env.EUCAPTCHA_SECRET}
cookie_secret {env.EUCAPTCHA_COOKIE_SECRET}
}
reverse_proxy localhost:3000
}
# Public — no challenge
route /public/* {
reverse_proxy localhost:3000
}
}
Behind a reverse proxy
The handler reads the client IP directly from Caddy's r.RemoteAddr. If Caddy itself sits behind another proxy (a load balancer, CDN, or ingress controller), configure Caddy's built-in trusted_proxies global option so that Caddy populates r.RemoteAddr with the real visitor IP before the eu_captcha handler runs:
{
servers {
trusted_proxies static 10.0.0.0/8
}
}
example.com {
route {
eu_captcha {
sitekey {env.EUCAPTCHA_SITEKEY}
secret {env.EUCAPTCHA_SECRET}
cookie_secret {env.EUCAPTCHA_COOKIE_SECRET}
}
reverse_proxy localhost:3000
}
}
Server-side verification
The handler calls the EU CAPTCHA API on every completed challenge. Responses with "train": true are treated as failures — access is not granted for training samples. For details on the verification response fields, see Server-Side Verification.
See also
- Traefik Plugin — EU CAPTCHA with CrowdSec in Traefik
- CrowdSec Bouncer — standalone Go reverse proxy with CrowdSec and EU CAPTCHA