Integrations
Caddy module
The ipscanner directive checks each visitor inside Caddy, before reverse_proxy or file_server.
Install
- 1Build Caddy with the module using xcaddy, or pick
github.com/ipscanner/ipscanner-caddyon caddyserver.com/download. - 2Add a Caddy site in the dashboard and copy its Site ID.
- 3Set
IPSCANNER_API_KEYin Caddy's environment. - 4Add the
ipscannerdirective with the Site ID to your site block.
Requires Caddy 2.8 or later. Check with caddy list-modules | grep ipscanner.
A new site starts in monitor mode: it adds headers and blocks nothing until you switch it to enforce.
Overview on the Caddy module page, source on GitHub.
xcaddy build --with github.com/ipscanner/ipscanner-caddyCaddyfile
ipscannerThe module orders it before
basic_auth, so no global order line is needed. It runs after header, redir and rewrites, before authentication,reverse_proxyandfile_server.ipscanner /app/* { ... }A matcher limits the check to some paths.
mode,block_classesWithout a
site_id, the Caddyfile sets the mode and the blocked classes."handler": "ipscanner"In JSON config, a handler with the same fields.
example.com {
ipscanner {
site_id site_...
}
reverse_proxy localhost:8080
}Configuration
api_keydefault {env.IPSCANNER_API_KEY}API key. Placeholders work, so the key can come from the environment or a file.
site_idSite ID from the dashboard. Mode and policy then come from the dashboard.
modedefault monitormonitor or enforce. With a site, only monitor has an effect: it keeps the site in monitor mode.
block_classesdefault malicious_automationClasses blocked in enforce mode without a site: malicious_automation, ai_agent, tor, vpn, proxy, hosting, relay or none.
timeoutdefault 1.5sBudget for one check. On a timeout the request passes.
cache_ttldefault 10mHow long a verdict is reused per site, IP and user agent.
cache_sizedefault 10000Verdicts kept in memory.
policy_ttldefault 30sHow long the site policy is reused.
skip_pathsdefault static assetsCase-insensitive regex on the path. Matching requests are not checked; none checks every path.
block_messagedefault Blocked by IPScanner edge guard.Text of the 403 page.
debugdefault offAdds
X-IPScanner-Status,-Classand-Actionto responses and logs one line per decision.api_urldefault https://ipscanner.ioAPI base URL.
The default skip_paths covers robots.txt, sitemap.xml, favicon.ico, scripts, stylesheets, source maps, images, fonts and video.
Client IP
The module uses the client IP Caddy resolved and never reads X-Forwarded-For itself. Without trusted_proxies that is the peer address.
trusted_proxiesTrust your load balancer or CDN in the global server options.
trusted_proxies_strictTakes the rightmost untrusted address, so a visitor cannot prepend a fake hop. Recommended.
client_ip_headers CF-Connecting-IPBehind Cloudflare: trust the Cloudflare ranges and read
CF-Connecting-IP.
{
servers {
trusted_proxies static 10.0.0.0/8
trusted_proxies_strict
}
}Headers it adds
Added to the request your app receives. Incoming X-IPScanner-* headers are removed first, so a client cannot send its own verdict.
X-IPScanner-Statusok, error, timeout, backoff, skippedWhether the check ran, and if not, why.
X-IPScanner-Classhuman, verified_bot, ai_agent, …Traffic class of the visitor.
X-IPScanner-Actionallow, flag, block, would_flag, would_blockWhat happened to the request. Monitor mode gives
would_flagandwould_block.X-IPScanner-Network-Classresidential_clean, hosting, vpn, tor, …Network class of the address.
X-IPScanner-Anonymizedtrue, falseThe network hides where the visitor is.
X-IPScanner-Risk0-100Risk score of the address.
X-IPScanner-CountryDECountry of the address.
X-IPScanner-Sitesite_...The Site ID, when one is set.
When a check fails or is skipped, only X-IPScanner-Status is set.
Placeholders
{http.vars.ipscanner.status}Status of the check, for logs and matchers after the directive.
{http.vars.ipscanner.class}Traffic class, set when the status is ok.
{http.vars.ipscanner.action}allow, flag, block,
would_flagorwould_block.
X-IPScanner-Status: ok
X-IPScanner-Class: ai_agent
X-IPScanner-Action: would_block
X-IPScanner-Network-Class: hosting
X-IPScanner-Anonymized: true
X-IPScanner-Risk: 60
X-IPScanner-Country: US
X-IPScanner-Site: site_4fQ8nZ2kLm7xR1vT9cBwCaching and fail open
- Site policyGET /v1/sites/{id}/policy
Mode and per-class policy, cached 30 s. If a refresh fails, the last good policy stays for 24 hours. Changes apply without a reload.
- Verdict cache
One check per visitor (site, IP, user agent) per 10 minutes, in memory, up to 10,000 entries. A config reload starts with an empty cache.
- Fail open
1.5 s for verdict and policy together. On a timeout or API error the request passes and checks pause for 30 s, or 5 minutes after a 401, 402, 403 or 429.
- Always pass
Verified crawlers, OPTIONS requests, private and loopback addresses, and paths on the skip list.
- Blocked
A 403 page with the request ID. Monitor mode never blocks.
- Free plan
Sites on the Free plan stay in monitor mode.
- Cost
2 request units per uncached visitor. Repeat requests inside the cache window are free.
What it sends
- POST /v1/edge/checkCounts as 2 requests
One call per uncached visitor.
- GET /v1/sites/{id}/policyNot metered
The site policy, when a Site ID is set.
X-IPScanner-Source: caddyMarks the calls as this integration's in your traffic and usage.
User-Agent: ipscanner-caddy/<version>The version, shown on the site's page in the dashboard.
Request body
sitestringThe Site ID, when one is set.
ipstringThe visitor's address.
user_agentstringThe visitor's User-Agent.
headersobjectAccept, Accept-Language, Accept-Encoding, Sec-CH-UA* and Sec-Fetch-*, each cut to 512 bytes.
request_idstringThe request's
X-Request-Idor a new ID, also printed on the block page.
curl https://ipscanner.io/v1/edge/check \
-H "Authorization: Bearer $IPSCANNER_API_KEY" \
-H "X-IPScanner-Source: caddy" \
-H "User-Agent: ipscanner-caddy/0.1.0" \
-H "Content-Type: application/json" \
-d '{
"site": "site_4fQ8nZ2kLm7xR1vT9cBw",
"ip": "203.0.113.7",
"user_agent": "Mozilla/5.0",
"headers": {
"accept-language": "en-US"
},
"request_id": "9f2c41b7d03e77f2"
}'