Forward auth for your server
One container next to your server checks every request for bots, Tor, VPNs and proxies. It starts in monitor mode and blocks nothing until you switch.
Free, MIT licenseDocker, amd64 and arm64
Traffic classes
Each class reaches your app as X-IPScanner-Class. Allow, flag or block it per site in the dashboard.
malicious_automation
ai_agent
tor
vpn
proxy
hosting
relay
verified_bot
human
Setup
The free plan is enough to start.
IPSCANNER_API_KEY
pk_live_••••••••3f9a
On the same Docker network as your proxy. Do not publish its port.
services:
ipscanner:
image: ghcr.io/ipscanner/forward-auth:0.1
restart: unless-stopped
environment:
IPSCANNER_API_KEY: ${IPSCANNER_API_KEY}
SITE_ID: site_...SITE_IDThe dashboard then holds the site's policy and shows its traffic.
SITE_ID
site_4fQ8nZ2kLm7xR1vT9cBw
Pick your server below. Each snippet is tested end to end.
Server config
Copy the one for your setup. Traefik and Caddy also work through the same container.
Goes in your server block. Start the gate first: nginx resolves ipscanner at startup.
Gate down: requests pass after 1 s
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;
}Settings
Set them on the container. CACHE_TTL and POLICY_TTL also take values like 10m.
| Variable | Default | What it does |
|---|---|---|
IPSCANNER_API_KEYYour API key. Without it every request passesDefault:Empty | Empty | Your API key. Without it every request passes |
IPSCANNER_API_KEY_FILEReads the key from a file, for Docker secretsDefault:Empty | Empty | Reads the key from a file, for Docker secrets |
SITE_IDLinks the gate to a dashboard site and its policyDefault:Empty | Empty | Links the gate to a dashboard site and its policy |
MODEmonitor or enforce. With SITE_ID, the dashboard decides unless this is monitorDefault:monitor | monitor | monitor or enforce. With SITE_ID, the dashboard decides unless this is monitor |
BLOCK_CLASSESClasses blocked in enforce mode when there is no SITE_IDDefault:malicious_automation | malicious_automation | Classes blocked in enforce mode when there is no SITE_ID |
TRUSTED_PROXIESProxies whose X-Forwarded-For is believedDefault:private | private | Proxies whose X-Forwarded-For is believed |
IP_HEADERSHeaders read for the visitor IP, in orderDefault:X-Forwarded-For | X-Forwarded-For | Headers read for the visitor IP, in order |
TIMEOUT_MSBudget per check, then the request passesDefault:1500 | 1500 | Budget per check, then the request passes |
CACHE_TTLSeconds a verdict is reused per visitorDefault:600 | 600 | Seconds a verdict is reused per visitor |
CACHE_SIZEVerdicts kept in memoryDefault:10000 | 10000 | Verdicts kept in memory |
POLICY_TTLSeconds a site policy is reusedDefault:30 | 30 | Seconds a site policy is reused |
SKIP_PATHSPath regex that skips the checkDefault:Static assets | Static assets | Path regex that skips the check |
UPSTREAM_URLTurns on proxy mode and forwards allowed requests hereDefault:Empty | Empty | Turns on proxy mode and forwards allowed requests here |
LISTENListen addressDefault::8080 | :8080 | Listen address |
HEALTH_PATHHealth endpointDefault:/healthz | /healthz | Health endpoint |
BLOCK_MESSAGEText of the 403 pageDefault:Blocked by IPScanner edge guard. | Blocked by IPScanner edge guard. | Text of the 403 page |
IPSCANNER_API_URLAPI base URLDefault:https://ipscanner.io | https://ipscanner.io | API base URL |
DEBUGLogs one line per decisionDefault:false | false | Logs one line per decision |
Container
GET /healthz always answers 200 and shows what the gate is doing.
{
"status": "ok",
"version": "0.1.0",
"mode": "forward_auth",
"enabled": true,
"site": "site_4fQ8nZ2kLm7xR1vT9cBw",
"backoff": false,
"policyMode": "monitor",
"policyVersion": 3,
"cachedVerdicts": 128
}ghcr.io/ipscanner/forward-auth:0.1
Questions
Run the gate, read a week of decisions, then block.