Block scanners at the edge with one Cloudflare rule
Stand up anything on the public internet and within minutes the scanners arrive.
They don't know or care what you're running — they just fire a fixed list of
"maybe someone left this lying around" requests at you: /.env, /.git/config,
/wp-login.php, /phpmyadmin, /backup.sql. Every one is a roll of the dice on
finding a secret you didn't mean to ship.
I saw @vikingmute share a clean way to deal with this, and it's worth passing on: block the whole class of requests at Cloudflare's edge, so they never touch your origin at all.
Why the edge
You could handle this at the app — return 404s, add middleware — but that still means the request reaches your server, burns a worker/process, and lands in your logs. Blocking at Cloudflare means:
- Zero origin load. The request dies at the edge.
- Clean logs. Your real traffic isn't buried under scanner noise.
- No exposure window. Even if a secret file is sitting there, the fetch never completes.
The rule
Cloudflare WAF custom rules take a boolean expression. Block the paths by
OR-ing a contains check for each:
(http.request.uri.path contains "/.env")
or (http.request.uri.path contains "/.git")
or (http.request.uri.path contains "/.aws")
or (http.request.uri.path contains "/wp-login")
or (http.request.uri.path contains "/phpmyadmin")
or (http.request.uri.path contains "/xmlrpc")
contains matches anywhere in the path, so /some/nested/.git/config is caught
too. Set the action to Block — there is no legitimate reason for a browser
to request /.env, so you don't need a softer challenge.
Where do the paths come from? The excellent
ayoubfathi/leaky-paths wordlist —
a deliberately lean, high-signal list of endpoints from modern stacks that tend
to leak. You don't want every CVE path (that's a losing game); you want the
handful scanners actually try first.
Build your own
Toggle the groups that match your stack and copy the expression. Skip the
WordPress / admin groups if you actually run those — otherwise you'll block
your own /wp-admin login.
(http.request.uri.path contains "/.env")
or (http.request.uri.path contains "/.git")
or (http.request.uri.path contains "/.svn")
or (http.request.uri.path contains "/.aws")
or (http.request.uri.path contains "/.ssh")
or (http.request.uri.path contains "/.config")
or (http.request.uri.path contains "/.DS_Store")
or (http.request.uri.path contains "/.htaccess")
or (http.request.uri.path contains "/.htpasswd")
or (http.request.uri.path contains "/wp-login")
or (http.request.uri.path contains "/wp-admin")
or (http.request.uri.path contains "/wp-includes")
or (http.request.uri.path contains "/wp-content")
or (http.request.uri.path contains "/xmlrpc")
or (http.request.uri.path contains "/phpmyadmin")
or (http.request.uri.path contains "/adminer")
or (http.request.uri.path contains "/server-status")
or (http.request.uri.path contains "/server-info")Cloudflare dashboard → Security → WAF → Custom rules → Create → paste as the expression → action Block.
Then: Cloudflare dashboard → Security → WAF → Custom rules → Create, switch to Edit expression, paste, action Block, deploy.
Verify
curl -s -o /dev/null -w "%{http_code}\n" https://yoursite.com/.env # → 403
curl -s -o /dev/null -w "%{http_code}\n" https://yoursite.com/ # → 200Takeaways
- Scanners hit a predictable set of leaky paths — block them as a class, not one 404 handler at a time.
- Do it at the edge (Cloudflare WAF custom rule) so requests never reach the origin: no load, no log noise, no exposure window.
- Keep the list lean and high-signal; tune the groups to your actual stack.
I packaged this up on the Freebies page — an interactive generator, plus a downloadable Claude Code skill that'll write and apply the rule for any zone.
Ask your agent to implement this
Read the full writeup at https://seangeng.com/writing/block-scanners-at-the-edge.md and implement it in my project.
It covers: Block scanners at the edge with one Cloudflare rule — Bots hammer every site for /.env, /.git, /wp-login and a hundred other 'leaky paths'. Here's a single Cloudflare WAF custom rule that blocks them before they ever reach your origin — plus a generator and a Claude Code skill to apply it.
Requirements:
- Follow the technique/approach exactly as described in the writeup.
- Adapt names, colors, and styling to my project's existing conventions.
- If it's a component, make it reusable with sensible props and TypeScript types.
- Keep it accessible: semantic HTML, keyboard support, and respect prefers-reduced-motion.
- When done, tell me which files you created or changed and how to use it.Paste into Claude Code, Codex, Cursor, or any agent. view raw .md