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" and http.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-IP header with real_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/.env should 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.

  • Cloudflare
  • Security
  • WAF