If your site runs behind Cloudflare, its WAF custom rules let you block or challenge unwanted traffic at Cloudflare’s edge, before it ever reaches your VPS. Even the free plan includes a handful of custom rules and basic rate limiting, which is enough to stop most brute-force, scanner and bot noise. Here are rules that work well for typical sites.
Where to find them
In the Cloudflare dashboard: your domain → Security → WAF → Custom rules → Create rule. Each rule has an expression (what to match) and an action:
| Action | Effect |
|---|---|
| Block | Returns 403 |
| Managed Challenge | Cloudflare decides: invisible check, or an interactive challenge if suspicious |
| JS Challenge | Requires JavaScript (stops simple bots) |
| Skip | Bypass other rules or features (for trusted traffic) |
| Log | Record only (paid plans) |
Rules are evaluated in order. You can use the expression builder or edit expressions directly. The examples below are expressions.
1. Always allow your own IPs and services
Put this first, with the Skip action (skip the remaining custom rules, and optionally rate limiting and managed rules):
(ip.src in {203.0.113.50 198.51.100.0/24})
Add your office, your monitoring service and webhooks from payment providers if they’d otherwise be challenged.
2. Protect WordPress login and XML-RPC
(http.request.uri.path eq "/xmlrpc.php")
Action: Block, if you don’t use XML-RPC (Jetpack and the mobile app do). See disable XML-RPC.
(http.request.uri.path eq "/wp-login.php") or (http.request.uri.path contains "/wp-admin/" and not http.request.uri.path contains "/wp-admin/admin-ajax.php")
Action: Managed Challenge. Bots fail, and real admins pass once and get a clearance cookie. Combine it with rule 1 so your own IP isn’t challenged.
3. Block common vulnerability probes
(http.request.uri.path contains "/.env") or
(http.request.uri.path contains "/.git") or
(http.request.uri.path contains "wp-config.php") or
(http.request.uri.path contains "/phpmyadmin") or
(http.request.uri.path contains "/vendor/phpunit") or
(http.request.uri.path matches "\\.(bak|sql|old|swp)$")
Action: Block. (matches, i.e. regex, is available on paid plans. Use several ends_with conditions on the free plan.)
4. Challenge or block by country
If your business only serves certain regions, and most attacks come from elsewhere:
(not ip.src.country in {"US" "CA" "GB" "DE"}) and (http.request.uri.path contains "/wp-login.php")
Action: Managed Challenge (or Block). Applying geo rules only to sensitive paths, rather than the whole site, avoids locking out legitimate visitors and search engines.
5. Bad bots and empty user agents
(http.user_agent eq "") or
(lower(http.user_agent) contains "sqlmap") or
(lower(http.user_agent) contains "nikto") or
(lower(http.user_agent) contains "masscan")
Action: Block.
Don’t block “good” bots by accident. Use cf.client.bot (Cloudflare’s verified bot flag) to exempt Googlebot and Bingbot from challenges:
(http.request.uri.path contains "/search") and not cf.client.bot
6. Rate limiting rules
Under Security → WAF → Rate limiting rules:
- Match:
http.request.uri.path eq "/wp-login.php"andhttp.request.method eq "POST" - Rate: 5 requests per 10 seconds per IP (the free plan has fixed periods)
- Action: Block for 10 seconds (longer durations on paid plans)
Apply the same to API login endpoints, password reset, search and contact forms. Keep server-side rate limits too. See Nginx rate limiting.
7. Lock down the origin
Cloudflare rules are worthless if attackers can reach your server’s IP directly. On the VPS:
- Allow HTTP/HTTPS only from Cloudflare’s IP ranges, using the published IPv4 and IPv6 lists:
for ip in $(curl -s https://www.cloudflare.com/ips-v4); do sudo ufw allow from "$ip" to any port 443 proto tcp; done
sudo ufw delete allow 443/tcp # remove the general rule
- Or use Cloudflare Tunnel, so the server has no open web ports at all. See Cloudflare Tunnel.
- Make sure Nginx logs the real visitor IP (the
CF-Connecting-IPheader withreal_ip_header), so Fail2Ban and logs see real clients. See Cloudflare with a VPS.
Test and monitor
- Security → Events shows which rules matched which requests. Check it after creating rules, to catch false positives.
- Test from another network:
curl -I https://example.com/.envshould return 403. - Start sensitive rules with Managed Challenge rather than Block, then tighten once you’re sure.
Quick reference
| Goal | Expression idea | Action |
|---|---|---|
| Trust own IPs | ip.src in {…} |
Skip |
| XML-RPC | uri.path eq "/xmlrpc.php" |
Block |
| Login protection | uri.path eq "/wp-login.php" |
Managed Challenge |
| Probes | uri.path contains "/.env" … |
Block |
| Geo on sensitive paths | not ip.src.country in {…} |
Challenge |
| Scanners | user_agent contains "sqlmap" |
Block |
Related: WAF explained and Fail2Ban for WordPress.