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
- 1Add a forward auth site in the dashboard and copy its Site ID.
- 2Run the gate on your proxy's Docker network with your API key and the Site ID. Do not publish its port.
- 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.
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
- nginx
auth_requestauth_requestasks/checkabout 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 Manager
auth_requestThe same, pasted into the proxy host's Advanced tab, with the gate on NPM's Docker network.
- Traefik
ForwardAuthA ForwardAuth middleware in your app's labels. The Traefik plugin does the same without the container.
- Caddy
forward_authforward_authto/check, copying theX-IPScanner-*headers. The Caddy module does the same without the container. - HAProxy
UPSTREAM_URLProxy mode with
UPSTREAM_URL=http://app:80. The app is the backup server, so traffic flows when the gate is down. - Apache
UPSTREAM_URLProxy 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 auth200 with the
X-IPScanner-*headers, or 403 with the block page andX-IPScanner-Request-Id./blockedforward authThe 403 page for the request ID in
X-IPScanner-Request-Id, for nginxerror_page./healthzboth modes200 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_KEYYour API key. Without one every request passes unchecked.
IPSCANNER_API_KEY_FILEFile holding the key, for Docker secrets. Wins over
IPSCANNER_API_KEY.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_automationWithout a site: classes blocked in enforce mode, comma separated, from malicious_automation, ai_agent, tor, vpn, proxy, hosting, relay or none.
TRUSTED_PROXIESdefault privateAddresses and CIDR ranges whose IP and URI headers are trusted. private, cloudflare, or none for the peer address only.
IP_HEADERSdefault X-Forwarded-ForHeaders read from a trusted proxy, in order. Behind Cloudflare:
CF-Connecting-IP.TIMEOUT_MSdefault 1500Budget for one check, in milliseconds. On a timeout the request passes.
CACHE_TTLdefault 600Seconds a verdict is reused per site, IP and user agent. Durations such as 10m work too.
CACHE_SIZEdefault 10000Verdicts kept in memory.
POLICY_TTLdefault 30Seconds the site policy is reused. Durations work too.
SKIP_PATHSdefault static assetsCase-insensitive regex on the path. Matching requests are not checked; empty checks every path.
UPSTREAM_URLTurns on proxy mode: allowed requests go to this URL.
LISTENdefault :8080Listen address.
HEALTH_PATHdefault /healthzHealth endpoint.
BLOCK_MESSAGEdefault Blocked by IPScanner edge guard.Text of the 403 page.
IPSCANNER_API_URLdefault https://ipscanner.ioAPI base URL.
DEBUGdefault falseLogs 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_PROXIESX-Forwarded-Foris 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.privateThe 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.
:8080Keep 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, 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.
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, 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.
- GET /v1/sites/{id}/policyNot metered
The site policy, when a Site ID is set.
X-IPScanner-Source: forward_authMarks 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
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: 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"
}'