Skip to content

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
  1. On each request the handler checks for a valid signed session cookie.
  2. Cookie valid — the request is forwarded to the next handler unchanged.
  3. No cookie / expired / wrong IP — the visitor is redirected to the built-in challenge page.
  4. The visitor completes the EU CAPTCHA widget; the browser posts the token to the verify endpoint.
  5. 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_captcha must be placed inside a route block so Caddy evaluates it before reverse_proxy. Without route, Caddy's handler ordering may apply reverse_proxy first.

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.

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 (default 1h)
  • 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

Source

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