Caddy module that checks visitors with IPScanner before a request reaches your site, and blocks bots, Tor, VPN and proxy traffic by policy.
Build Caddy with the module:
xcaddy build --with github.com/ipscanner/ipscanner-caddyOr pick github.com/ipscanner/ipscanner-caddy on caddyserver.com/download. With Docker:
FROM caddy:2-builder AS builder
RUN xcaddy build --with github.com/ipscanner/ipscanner-caddy
FROM caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddyRequires Caddy 2.8 or later. Check with caddy list-modules | grep ipscanner.
Create an API key at ipscanner.io and a site in the dashboard, then:
example.com {
ipscanner {
site_id site_...
}
reverse_proxy localhost:8080
}The key is read from IPSCANNER_API_KEY in Caddy's environment. Mode and per class policy then come from the dashboard, and changes apply without a reload.
The directive is ordered before basic_auth, so no global order line is needed: it runs after header, redir and rewrites, and before authentication, reverse_proxy and file_server. A matcher limits it to some paths: ipscanner /app/* { ... }.
Without a site, the Caddyfile sets mode and blocked classes:
ipscanner {
mode enforce
block_classes malicious_automation tor
}| Classes | human, verified_bot, ai_agent, malicious_automation, tor, vpn, proxy, hosting, relay |
Without site_id |
mode (default monitor) and block_classes (default malicious_automation) from the Caddyfile. The network class counts too, so blocking vpn also blocks a bot on a VPN. |
With site_id |
Mode and per class policy (allow, flag, block) from GET /v1/sites/{id}/policy, cached 30 s, last good policy kept for 24 h if a refresh fails. mode monitor keeps the module in monitor mode whatever the dashboard says. |
| Always pass | Verified crawlers, OPTIONS, private and loopback addresses, paths matching skip_paths. |
| Cache | One check per visitor (site, IP, user agent) per 10 minutes, in memory, bounded to 10000 entries. A config reload starts with an empty cache. |
| Fail open | One 1.5 s budget for verdict and policy. On a timeout or error the request passes and checks pause for 30 s (5 minutes on 401, 402, 403 or 429). |
| Blocked | 403 page with Caddy's request ID. monitor never blocks: the outcome is would_block or would_flag. |
| Visitor IP | The client IP Caddy resolved ({client_ip}), so the server's trusted_proxies apply. See Client IP. |
| Debug | debug adds X-IPScanner-Status, -Class and -Action to responses and logs every decision. |
Incoming X-IPScanner-* headers are removed, so clients cannot send a verdict of their own. Requests to the API send Authorization: Bearer <key>, X-IPScanner-Source: caddy and User-Agent: ipscanner-caddy/<version>.
| Subdirective | Default | Description |
|---|---|---|
api_key |
{env.IPSCANNER_API_KEY} |
API key. Placeholders work: {env.NAME}, {file./run/secrets/ipscanner}. Without a key every request passes with status skipped. |
site_id |
Dashboard site (site_...). Mode and policy then come from the dashboard. |
|
mode |
monitor |
monitor or enforce. With site_id only monitor has an effect. |
block_classes |
malicious_automation |
Classes blocked in enforce mode without site_id. Any of malicious_automation, ai_agent, tor, vpn, proxy, hosting, relay, or none. |
timeout |
1.5s |
Budget for one check. |
cache_ttl |
10m |
Verdict cache per site, IP and user agent. |
cache_size |
10000 |
Verdict cache entries. |
policy_ttl |
30s |
Site policy cache. |
skip_paths |
static assets | Case insensitive regex on the path as the visitor sent it. none checks every path. |
block_message |
Blocked by IPScanner edge guard. |
Text of the 403 page. |
debug |
off | Response debug headers and one log line per decision. |
api_url |
https://ipscanner.io |
API base URL. |
The default skip_paths is robots.txt, sitemap.xml, favicon.ico and files ending in .css, .js, .mjs, .map, images, fonts, .mp4 and .webm.
JSON config, the same fields:
{
"handler": "ipscanner",
"site_id": "site_...",
"timeout": "1.5s",
"block_classes": ["malicious_automation", "tor"]
}The module uses Caddy's resolved client IP and does not read X-Forwarded-For itself. Without trusted_proxies that is the peer address. Behind a load balancer or CDN, trust it in the global options:
{
servers {
trusted_proxies static 10.0.0.0/8
trusted_proxies_strict
}
}trusted_proxies_strict takes the rightmost untrusted address, as the Worker and the WordPress plugin do, so a visitor cannot prepend a fake hop. Behind Cloudflare, trust the Cloudflare ranges and add client_ip_headers CF-Connecting-IP.
Added to the request for the next handler and the upstream:
| Header | Example |
|---|---|
X-IPScanner-Status |
ok, error, timeout, backoff or skipped |
X-IPScanner-Class |
human, verified_bot, vpn, ... |
X-IPScanner-Action |
allow, flag, block, would_flag, would_block |
X-IPScanner-Network-Class |
residential_clean, hosting, vpn, tor, ... |
X-IPScanner-Anonymized |
true or false |
X-IPScanner-Risk |
0 to 100 |
X-IPScanner-Country |
DE |
X-IPScanner-Site |
site_..., set with site_id |
When a check fails or is skipped, only X-IPScanner-Status is set.
| Placeholder | Values |
|---|---|
{http.vars.ipscanner.status} |
ok, error, timeout, backoff, skipped |
{http.vars.ipscanner.class} |
Traffic class, set when the status is ok |
{http.vars.ipscanner.action} |
allow, flag, block, would_flag, would_block |
For access logs and matchers after the directive:
log_append ipscanner_class {http.vars.ipscanner.class}
log_append ipscanner_action {http.vars.ipscanner.action}
@flagged vars ipscanner.action flag
request_header @flagged X-Review 1go test -race ./...
IPSCANNER_KEY_FILE=/path/to/key IPSCANNER_E2E_SITE_HELPER=/path/to/site.cjs sh test/e2e/run.shThe end to end run builds Caddy with xcaddy in the caddy:2.11-builder image, starts it with Docker Compose in front of an echo upstream and sends requests as different visitors. It needs an API on IPSCANNER_API_URL (default http://localhost:3050) and a helper script that upserts test sites and lists request log rows.
The decision logic lives in ipscanner-edge-go, shared with the Traefik plugin and the forward auth gate.
API reference: ipscanner.io/api-documentation.
MIT