How to block VPN, proxy and Tor traffic
Working rules for Cloudflare WAF, Nginx, Caddy, and Stripe Radar — plus the false-positive playbook. Most VPN traffic is legitimate, so blunt blocks cost you customers. Use these patterns to block only where the friction is worth it.
Decision: where to block, where to step up
- Block: account creation, KYC submission, money-out flows (withdrawals, refunds), admin/staff routes.
- Step up auth: login, password reset, payment, subscription change, bulk operations.
- Allow: read traffic, RSS feeds, public docs. Hard-blocking these costs more than the abuse you'd prevent.
Cloudflare WAF expression
Cloudflare's built-in cf.threat_scoreand ip.src.is_in_european_uniondon't catch enough on their own. Layer in an external lookup via Cloudflare Worker (see implementation guide), then enforce with a WAF rule:
# Block VPN/proxy on signup + checkout
(http.request.uri.path matches "^/(signup|checkout|api/payment)" and
cf.threat_score gt 14 or
ip.src.is_in_european_union eq false and any(http.request.headers["x-iplogs-verdict"][*] in {"vpn_detected" "vpn_likely"}))Set the x-iplogs-verdict header from a Worker that calls POST /v1/check on the visitor IP.
Nginx — block by ASN list
For static blocking by ASN, mount the IPLogs CSV at /data/datacenter-asns.csv and convert to an Nginx geo block:
# Generate /etc/nginx/conf.d/vpn-block.conf from the IPLogs ASN list
geo $is_vpn_asn {
default 0;
# Add CIDR ranges for AS9009 (M247), AS62240 (Clouvider), AS9009 etc.
# Pull from https://iplogs.com/data/datacenter-asns.csv
23.234.0.0/16 1;
185.65.0.0/16 1;
# ...
}
server {
location ~ ^/(signup|checkout) {
if ($is_vpn_asn) {
return 403 "VPN traffic blocked on this route";
}
proxy_pass http://app;
}
}Caddy — header-based block via reverse-proxy
# Caddyfile snippet — call IPLogs from a Caddy plugin or
# pre-set the verdict header from your app middleware
example.com {
@vpn_block {
path /signup* /checkout*
header X-IPLogs-Verdict vpn_detected vpn_likely
}
respond @vpn_block 403 "VPN traffic blocked on this route"
reverse_proxy localhost:3000
}Stripe Radar — rule recipe
Stripe Radar 2.0 already scores VPN/proxy IPs as elevated risk by default. To enforce harder rules, pass the IPLogs verdict into the Customer.metadata at payment intent creation:
// On payment intent creation
const verdict = (await checkVpn(customerIp)).verdict;
await stripe.paymentIntents.create({
amount, currency,
customer: customerId,
metadata: { iplogs_verdict: verdict },
});
// Stripe Radar rule (UI):
// Block if :metadata:iplogs_verdict: == "vpn_detected"
// Review if :metadata:iplogs_verdict: == "vpn_likely"
// 3DS if :metadata:iplogs_verdict: == "suspicious" and :amount: > 5000False-positive playbook
Before you block, plan how to handle false positives. Most "false positive" reports come from these populations:
- CGNAT / mobile carriers — shared IP pools that overlap with commercial VPN exits. Step-up over hard-block.
- Cloudflare WARP / iCloud Private Relay — most users have these on by default. Treat as a separate "private_relay" tier with much lower friction.
- Corporate proxies — every enterprise customer routes traffic through a centralised egress that can look like a datacenter exit.
- Travelling users — business travellers commonly use VPN for work, and would be your highest-LTV customer to wrongly block.
Always include a path to dispute. A "VPN detected — try again on a different network" message that links to a contact form catches lost revenue from false positives.
Building VPN detection in code first? See implementation guide. Wondering why your IP shows as a VPN? Read the troubleshooting walkthrough.