IPScanner

Integrations

Caddy module

The ipscanner directive checks each visitor inside Caddy, before reverse_proxy or file_server.

Install

  1. 1Build Caddy with the module using xcaddy, or pick github.com/ipscanner/ipscanner-caddy on caddyserver.com/download.
  2. 2Add a Caddy site in the dashboard and copy its Site ID.
  3. 3Set IPSCANNER_API_KEY in Caddy's environment.
  4. 4Add the ipscanner directive 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-caddy

Caddyfile

  • ipscanner

    The module orders it before basic_auth, so no global order line is needed. It runs after header, redir and rewrites, before authentication, reverse_proxy and file_server.

  • ipscanner /app/* { ... }

    A matcher limits the check to some paths.

  • mode, block_classes

    Without 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_id

    Site ID from the dashboard. Mode and policy then come from the dashboard.

  • modedefault monitor

    monitor or enforce. With a site, only monitor has an effect: it keeps the site in monitor mode.

  • block_classesdefault malicious_automation

    Classes blocked in enforce mode without a site: malicious_automation, ai_agent, tor, vpn, proxy, hosting, relay or none.

  • timeoutdefault 1.5s

    Budget for one check. On a timeout the request passes.

  • cache_ttldefault 10m

    How long a verdict is reused per site, IP and user agent.

  • cache_sizedefault 10000

    Verdicts kept in memory.

  • policy_ttldefault 30s

    How long the site policy is reused.

  • skip_pathsdefault static assets

    Case-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 off

    Adds X-IPScanner-Status, -Class and -Action to responses and logs one line per decision.

  • api_urldefault https://ipscanner.io

    API 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_proxies

    Trust your load balancer or CDN in the global server options.

  • trusted_proxies_strict

    Takes the rightmost untrusted address, so a visitor cannot prepend a fake hop. Recommended.

  • client_ip_headers CF-Connecting-IP

    Behind Cloudflare: trust the Cloudflare ranges and read CF-Connecting-IP.

Caddyfile
{
	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, skipped

    Whether 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_block

    What happened to the request. Monitor mode gives would_flag and would_block.

  • X-IPScanner-Network-Classresidential_clean, hosting, vpn, tor, …

    Network class of the address.

  • X-IPScanner-Anonymizedtrue, false

    The network hides where the visitor is.

  • X-IPScanner-Risk0-100

    Risk score of the address.

  • X-IPScanner-CountryDE

    Country 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_flag or would_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_4fQ8nZ2kLm7xR1vT9cBw

Caching and fail open

  • 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.

  • The site policy, when a Site ID is set.

  • X-IPScanner-Source: caddy

    Marks 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

  • sitestring

    The Site ID, when one is set.

  • ipstring

    The visitor's address.

  • user_agentstring

    The visitor's User-Agent.

  • headersobject

    Accept, Accept-Language, Accept-Encoding, Sec-CH-UA* and Sec-Fetch-*, each cut to 512 bytes.

  • request_idstring

    The request's X-Request-Id or a new ID, also printed on the block page.

The edge check it makes
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"
  }'