IPScanner

Integrations

Forward auth for nginx, Apache and HAProxy

One small container that nginx, Nginx Proxy Manager, Apache, HAProxy, Traefik or Caddy asks about each request.

Install

  1. 1Add a forward auth site in the dashboard and copy its Site ID.
  2. 2Run the gate on your proxy's Docker network with your API key and the Site ID. Do not publish its port.
  3. 3Add the snippet for your proxy.

ghcr.io/ipscanner/forward-auth is a static binary for linux/amd64 and linux/arm64 that runs as non-root with no shell. Binaries for Linux and macOS are on the releases page.

A new site starts in monitor mode: it adds headers and blocks nothing until you switch it to enforce.

Overview on the nginx, Apache and HAProxy page, source on GitHub.

docker-compose.yml
services:
  ipscanner:
    image: ghcr.io/ipscanner/forward-auth:0.1
    restart: unless-stopped
    environment:
      IPSCANNER_API_KEY: ${IPSCANNER_API_KEY}
      SITE_ID: site_...

Proxy snippets

  • nginxauth_request

    auth_request asks /check about each request. A blocked visitor gets the gate's 403 page, and an unreachable gate lets traffic through. nginx resolves the gate at startup, so start the gate first.

  • Nginx Proxy Managerauth_request

    The same, pasted into the proxy host's Advanced tab, with the gate on NPM's Docker network.

  • TraefikForwardAuth

    A ForwardAuth middleware in your app's labels. The Traefik plugin does the same without the container.

  • Caddyforward_auth

    forward_auth to /check, copying the X-IPScanner-* headers. The Caddy module does the same without the container.

  • HAProxyUPSTREAM_URL

    Proxy mode with UPSTREAM_URL=http://app:80. The app is the backup server, so traffic flows when the gate is down.

  • ApacheUPSTREAM_URL

    Proxy mode as for HAProxy, with the app as a hot standby.

location / {
    auth_request /_ipscanner;
    auth_request_set $ipscanner_status     $upstream_http_x_ipscanner_status;
    auth_request_set $ipscanner_class      $upstream_http_x_ipscanner_class;
    auth_request_set $ipscanner_action     $upstream_http_x_ipscanner_action;
    auth_request_set $ipscanner_network    $upstream_http_x_ipscanner_network_class;
    auth_request_set $ipscanner_anonymized $upstream_http_x_ipscanner_anonymized;
    auth_request_set $ipscanner_risk       $upstream_http_x_ipscanner_risk;
    auth_request_set $ipscanner_country    $upstream_http_x_ipscanner_country;
    auth_request_set $ipscanner_site       $upstream_http_x_ipscanner_site;
    auth_request_set $ipscanner_request_id $upstream_http_x_ipscanner_request_id;
    error_page 403 = @ipscanner_blocked;

    proxy_set_header X-IPScanner-Status        $ipscanner_status;
    proxy_set_header X-IPScanner-Class         $ipscanner_class;
    proxy_set_header X-IPScanner-Action        $ipscanner_action;
    proxy_set_header X-IPScanner-Network-Class $ipscanner_network;
    proxy_set_header X-IPScanner-Anonymized    $ipscanner_anonymized;
    proxy_set_header X-IPScanner-Risk          $ipscanner_risk;
    proxy_set_header X-IPScanner-Country       $ipscanner_country;
    proxy_set_header X-IPScanner-Site          $ipscanner_site;
    proxy_set_header Host                      $host;
    proxy_set_header X-Forwarded-For           $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto         $scheme;
    proxy_pass http://app:80;
}

location = /_ipscanner {
    internal;
    proxy_pass http://ipscanner:8080/check;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Original-URI $request_uri;
    proxy_set_header X-Original-Method $request_method;
    proxy_connect_timeout 1s;
    proxy_read_timeout 5s;
    error_page 500 502 503 504 = @ipscanner_down;
}

location @ipscanner_down {
    return 204;
}

location @ipscanner_blocked {
    internal;
    proxy_method GET;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-IPScanner-Request-Id $ipscanner_request_id;
    rewrite ^ /blocked break;
    proxy_pass http://ipscanner:8080;
}

Endpoints

  • /checkforward auth

    200 with the X-IPScanner-* headers, or 403 with the block page and X-IPScanner-Request-Id.

  • /blockedforward auth

    The 403 page for the request ID in X-IPScanner-Request-Id, for nginx error_page.

  • /healthzboth modes

    200 JSON: version, mode, site, backoff, last error and the cached policy.

/check reads the original request from the proxy's headers: X-Forwarded-For (or IP_HEADERS), User-Agent, X-Forwarded-Uri or X-Original-URI, and X-Forwarded-Method or X-Original-Method.

Configuration

  • IPSCANNER_API_KEY

    Your API key. Without one every request passes unchecked.

  • IPSCANNER_API_KEY_FILE

    File holding the key, for Docker secrets. Wins over IPSCANNER_API_KEY.

  • 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

    Without a site: classes blocked in enforce mode, comma separated, from malicious_automation, ai_agent, tor, vpn, proxy, hosting, relay or none.

  • TRUSTED_PROXIESdefault private

    Addresses and CIDR ranges whose IP and URI headers are trusted. private, cloudflare, or none for the peer address only.

  • IP_HEADERSdefault X-Forwarded-For

    Headers read from a trusted proxy, in order. Behind Cloudflare: CF-Connecting-IP.

  • TIMEOUT_MSdefault 1500

    Budget for one check, in milliseconds. On a timeout the request passes.

  • CACHE_TTLdefault 600

    Seconds a verdict is reused per site, IP and user agent. Durations such as 10m work too.

  • CACHE_SIZEdefault 10000

    Verdicts kept in memory.

  • POLICY_TTLdefault 30

    Seconds the site policy is reused. Durations work too.

  • SKIP_PATHSdefault static assets

    Case-insensitive regex on the path. Matching requests are not checked; empty checks every path.

  • UPSTREAM_URL

    Turns on proxy mode: allowed requests go to this URL.

  • LISTENdefault :8080

    Listen address.

  • HEALTH_PATHdefault /healthz

    Health endpoint.

  • BLOCK_MESSAGEdefault Blocked by IPScanner edge guard.

    Text of the 403 page.

  • IPSCANNER_API_URLdefault https://ipscanner.io

    API base URL.

  • DEBUGdefault false

    Logs one line per decision. In proxy mode it also adds the status, class and action headers to responses.

Invalid values stop the gate at startup with a log line. The API key is never logged.

Client IP

  • TRUSTED_PROXIES

    X-Forwarded-For is read only when the peer is in this list, walking the chain back to the first address that is not a trusted proxy. From anyone else the peer address is used.

  • private

    The default. It fits a proxy that reaches the gate over a Docker or private network. If others on that network can reach the gate, list your proxy's address instead.

  • :8080

    Keep the gate's port unpublished: only your proxy should reach it.

Headers it adds

On a 200 from /check, for your proxy to copy to the app, and on the request to your app in proxy mode. Proxy mode removes incoming X-IPScanner-* headers; the snippets do the same in the proxy.

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

Request to your app
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, per gate instance.

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

  • Gate down

    nginx and NPM pass after the 1 s connect timeout, HAProxy sends traffic to the app at once, Apache fails one request and then uses the app. Traefik answers 500 and Caddy 502, so run the gate with restart: unless-stopped.

  • 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: forward_auth

    Marks the calls as this integration's in your traffic and usage.

  • User-Agent: ipscanner-forward-auth/<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: forward_auth" \
  -H "User-Agent: ipscanner-forward-auth/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"
  }'