Mr. Nice Try Notes from the edge

· 7 min read

Block credential probes. Every one of them.

In 12 days, 10,958 addresses asked the websites behind my edge for .env files, cloud keys and coding-agent settings 3.78 million times. 99.7% of those requests came from an address that had already asked once. Here's why the first ask is all the proof you need, and copy-paste rules to slow, limit and ban them in Cloudflare, nginx, Caddy and Laravel.

There's a kind of request that no real visitor ever makes.

Nobody clicking around your website asks for /.env. Nobody's browser fetches /.aws/credentials, /.git/config or /.claude/settings.json. Those are files on a developer's machine or a server's disk. They're not pages. The only reason to ask a website for them is to find out whether it will hand them over.

So when one of those requests arrives, you've been told something for free: whoever sent it is looking for your keys. You don't have to guess, score or fingerprint anything. You can act on it.

Most sites don't. They answer with a 404 and carry on serving the same address whatever it asks for next. This post is about doing more than that.

Mr. Nice Try holds up a stopwatch with a coffee in his other hand, behind a little velvet rope, while a line of small robots runs up to it.

3,780,041credential probes in 12 days
10,958addresses sending them
99.7%from an address that had already asked
<1 smedian wait before the second ask

A free signal

I run the Mr. Nice Try Edge in front of more than 3,600 websites. From October 8 to 19, it saw 3,780,041 requests for credential files, from 10,958 addresses in 4,344 networks, for 7,308 different paths.

The top of the list is what you'd expect:

Path Requests
/.env 67,116
/.git/config 44,666
/.env.local 42,566
/.env.production 41,971
/.env.backup 39,683
/api/.env 36,159
/backend/.env 34,959
/app/.env 34,928

Mr. Nice Try Edge, October 8–19, 2026 (UTC). A "credential probe" is a request for a secrets file: .env in any spelling, .git/config, .aws/, .ssh/, .npmrc, .pgpass, .docker/config.json, backups of wp-config.php, and coding agents' settings in .claude/, .codex/ and .cursor/. I stopped on October 19 because from October 20 the Edge was blocking hard for the PHP CVEs, which hides some of what came next.

The long tail is the point. 7,308 paths means 7,308 guesses about where a developer might have left a key: /laravel/.env, /storage/.env, /.env.staging, /.aws/credentials.bak. Coding agents are on the list now too: 34,712 of those requests were for .claude, .codex, .cursor or mcp.json files. That's the AI key hunt, and it's why the volume keeps climbing: on the sites behind the Edge, their own logs counted 1,921,276 probes of every kind in October, against 67,433 in September.

"What if it's a real person?"

It doesn't matter.

A person who types /.env into your address bar is doing the same thing a bot is: checking whether your secrets are lying around. You don't owe them a page. The worst that happens if you block them is that they can't load your home page for a while, from that address, after asking for your keys.

Mr. Nice Try at a front desk, calmly pointing a queue of visitors and little robots from the dark door on the left to the white door on the right.

And in practice it isn't people. Going by the names of the networks they came from, at least 98% of those 3.78 million probes came from cloud, hosting and data-center addresses, not homes or phones. The rest is mostly smaller hosting companies with less obvious names.

The one real risk is a shared address. A ban on one IP can catch other people behind the same one: an office, a VPN exit, a mobile carrier that puts thousands of phones behind a single address. That's why the ban below lasts days, not forever, and why the response is a plain 404, not a page that says "you've been banned". I wrote more about that trade-off in filtering abusers without casualties.

They don't stop at one

Here's the number that should change how you handle the first one: 99.7% of credential probes came from an address that had already sent one.

From the first probe of each address Addresses
Asked once, never again 2,051 (18.7%)
Still sending requests an hour later 6,439 (59%)
A day later 4,855 (44%)
Three days later 3,077 (28%)
A week later 413 (4%)

Same window and definition as above. "Still sending" means any request at all from that address, to any site behind the Edge. An address first seen near the end of the 12 days can't show up days later, so the longer rows are minimums.

The median address sent 8 credential probes. One sent 63,193. The second probe usually arrived less than a second after the first. A probe isn't a one-off. It's the first line of a list, and the rest of the list is already on its way.

That's what makes the first request so useful. If you ban the address the moment it asks for /.env, you don't have to recognise the other 7,307 paths. You never see them.

The ladder

There are five things you can do, from least effort to most. Do as many as your setup allows.

  1. Answer with a 404. The minimum. Make sure the file isn't there and the request gets nothing. If your site answers every unknown path with your home page and a 200, the bot can't tell what it found, and neither can you.
  2. Slow it down. Hold the answer for a few seconds before you send it. But only where holding a connection is cheap, like nginx or a CDN. Never in your application (more on that below).
  3. Rate-limit it. After the first probe, let that address have almost nothing more on these paths.
  4. Ban the address on the first ask. Every request from it, on every path, for a few days. A week outlasted all but 4% of the addresses in the table above.
  5. Ban the network. Some networks send almost nothing else. I listed four in Ban these networks right now, with their ranges.

The pattern

Every rule below uses the same idea: a dot-folder or dot-file at the start of a path segment, from a short list of names, plus backups of wp-config.php.

(^|/)\.(env|git|aws|ssh|docker|npmrc|pypirc|netrc|pgpass|my\.cnf|vault-token|claude|codex|cursor)(/|$|[._~-])|wp-config\.php.

It matches /.env, /.env.local, /api/.env, /.git/config, /.git-credentials, /.aws/credentials, /.ssh/id_rsa, /.claude/settings.json and /wp-config.php.bak. It leaves /.well-known/, /.github/, /.gitignore, /environment and /wp-config.php itself alone. Add the names your own stack uses.

How to block them

As with the network bans, the earlier the better. A request stopped at your CDN costs you nothing. The same request stopped in your app still woke your app up.

Cloudflare

Go to Security → WAF → Custom rules, create a rule, choose Edit expression, paste this, and set the action to Block:

(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 "/.ssh/")
or (http.request.uri.path contains "/.docker/")
or (http.request.uri.path contains "/.npmrc")
or (http.request.uri.path contains "/.pgpass")
or (http.request.uri.path contains "/.claude/")
or (http.request.uri.path contains "/.codex/")
or (http.request.uri.path contains "/.cursor/")
or (http.request.uri.path contains "wp-config.php.")

That stops every probe, but it doesn't touch the address's other requests. To ban the address itself, add a rate limiting rule that counts requests matching the same expression, by IP, with a threshold of 1, and blocks for as long as your plan allows. Plans differ in how long that can be, and in whether the block can cover all of an address's requests or only the matching ones, so check what yours offers.

nginx

nginx can do steps 2 to 4 itself. In the http {} block:

limit_req_zone $binary_remote_addr zone=probes:10m rate=1r/m;

In each server {} block, before your other locations:

location ~* "(^|/)\.(env|git|aws|ssh|docker|npmrc|pypirc|netrc|pgpass|my\.cnf|vault-token|claude|codex|cursor)(/|$|[._~-])|wp-config\.php." {
    limit_req zone=probes burst=5;
    limit_req_status 429;
    access_log /var/log/nginx/probes.log;
    deny all;
}

deny all answers 403. The limit_req line is the slow-down: without nodelay, the first probe is answered straight away, the next five wait their turn at one a minute, and anything beyond that gets a 429. Waiting costs nginx almost nothing: it holds the connection without blocking anything else.

For the ban, point fail2ban at that log. Every line in it is a probe, so the filter only has to find the address. Create /etc/fail2ban/filter.d/credential-probes.conf:

[Definition]
failregex = ^<HOST> -

And add a jail to /etc/fail2ban/jail.local:

[credential-probes]
enabled  = true
port     = http,https
filter   = credential-probes
logpath  = /var/log/nginx/probes.log
maxretry = 1
findtime = 10m
bantime  = 7d

maxretry = 1 is the ban on the first ask. Behind a load balancer or CDN, set up real_ip_header and set_real_ip_from first, or you'll ban your own proxy.

Caddy

Caddy matches paths with a regular expression too:

@probes path_regexp (^|/)\.(env|git|aws|ssh|docker|npmrc|pypirc|netrc|pgpass|my\.cnf|vault-token|claude|codex|cursor)(/|$|[._~-])|wp-config\.php.
handle @probes {
    respond 404
}

Put it at the top of your site block. Caddy has no rate limiting or banning built in. For those, log the matching requests and ban from the log as above, or add a rate limiting plugin.

Laravel

If your app is the first thing a request reaches, a middleware can ban the address on its first probe. It uses your cache, so on more than one server, use a shared store like Redis:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;

class BanCredentialProbes
{
    private const PROBE = '#(^|/)\.(env|git|aws|ssh|docker|npmrc|pypirc|netrc|pgpass|my\.cnf|vault-token|claude|codex|cursor)(/|$|[._~-])|wp-config\.php.#i';

    public function handle(Request $request, Closure $next): Response
    {
        $key = 'probe-ban:'.$request->ip();

        if (Cache::has($key)) {
            abort(404);
        }

        if (preg_match(self::PROBE, $request->path())) {
            Cache::put($key, true, now()->addDays(7));

            abort(404);
        }

        return $next($request);
    }
}

Register it in bootstrap/app.php with $middleware->prepend(BanCredentialProbes::class); so it runs before anything else. Behind a load balancer or CDN, set up trusted proxies first, or every visitor shares the proxy's address and one probe bans everyone.

Notice what it doesn't do: it doesn't sleep before the 404.

Don't do this

Don't slow them down in your app. A sleep(5) in PHP, Node or Python holds a worker for five seconds. These scanners send requests in parallel, so a slow answer costs them one connection out of hundreds and costs you a worker you needed for real visitors. Slow them down only where waiting is free: nginx, a CDN, a proxy built for it. In your app, answer fast and ban.

Don't feed them fake secrets. It's tempting to serve a .env full of decoy keys. But a believable fake teaches the bot your site is worth a closer look, and an unbelievable one teaches it nothing. If you want to know when a key is used, use a proper canary token from a service built for it, and don't put it where it can be mistaken for a real one.

Don't stop at the block. A block covers the paths you thought of. Your web root should still be your build or public folder, not your project folder, so the file isn't there to find in the first place. I went through that in the Railway post.

Check your own site

Run this against your own domain:

for p in .env .env.local api/.env .git/config .aws/credentials .ssh/id_rsa .npmrc .claude/settings.json .cursor/mcp.json wp-config.php.bak; do
  printf '%-24s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code} %{size_download}B' "https://example.com/$p")"
done

You want 403, 404 or 429 for every line. If anything says 200, open it. If it's your home page, your site answers everything with 200: not a leak, but it hides the one that is. If it's the file, take it down and rotate everything in it. Run it again a minute later: if your ban works, the requests after the first should all fail, including one for your home page.

The short version

  • No real visitor asks for /.env. The first credential probe tells you everything you need to know.
  • Ban on the first ask. 99.7% of probes came from an address that had already sent one.
  • Ban for days, not minutes. At least 44% of addresses were still around a day later, and 4% a week later.
  • Slow them down only where it's cheap. nginx and CDNs, yes. Your app's workers, no.
  • Answer with a plain 404, so a shared address that gets caught isn't told why.
  • Ban the worst networks outright, and keep the files out of your web root anyway.

The volume is going up, not down. I'm watching what knocks on more than 3,600 websites every day, and I'll keep posting what's on the list. I'm @MrNiceTry on X.

How I counted: requests in the Mr. Nice Try Edge's logs for the sites behind it, October 8–19, 2026 (UTC), with the path rule described above. "Networks" are /24 blocks. Hosting share is by the name of each address's network, so it's a floor. October and September totals are from the sites' own logs, May 1 to October 22, counting probes of every kind.