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.
FAQ
What's the best way to block VPN traffic on a website?
Block at the edge (Cloudflare WAF, Nginx, or Caddy) on high-risk routes — signup, KYC, withdrawals, admin — rather than across all traffic. Read traffic almost never benefits from a hard block. The standard pattern is to set a verdict header at the edge and reference it in your WAF rules.
Should I hard-block VPN users or just flag them?
Flag everywhere, hard-block only on high-risk actions. Most VPN traffic is legitimate (privacy, untrusted Wi-Fi, geo-content). Block writes (signup, payment, password reset) and step up auth on edge cases. Hard-blocking read traffic punishes loyal users for an attacker pattern.
How do I block VPNs on Cloudflare?
Run a Cloudflare Worker that calls the IPLogs API, writes the verdict into an X-IPLogs-Verdict header, then add a WAF rule: when the header equals vpn_detected or vpn_likely on protected paths, return a 403 or challenge. The full rule template is in the Cloudflare WAF section above.
How do I block VPN traffic in Nginx or Caddy?
Use a geo block keyed off ASN: maintain an IP/ASN list of known VPN exits and reject matching addresses on protected locations. For dynamic per-IP decisions, call the IPLogs API from a reverse-proxy header check and gate on the returned verdict. Examples for both servers are above.
What HTTP status should I return when blocking VPN traffic?
Use 403 Forbidden with a short page that explains the connection looks anonymized and offers a dispute path. Avoid 429 (rate-limit) or 503 (server error) — both confuse retry logic and bots, and 403 is the only semantically correct code for a policy-based refusal.
How do I avoid false positives when blocking VPNs?
Treat Apple Private Relay, Cloudflare WARP, CGNAT mobile networks, and corporate VPNs as separate categories, not as commercial VPN. Always offer a dispute path and log the decision so you can audit it. The IPLogs API returns these as distinct signals so your policy can apply different rules.
Building VPN detection in code first? See implementation guide. Wondering why your IP shows as a VPN? Read the troubleshooting walkthrough. See the accuracy benchmark for false-positive rates.