**TL;DR — Blocking individual IPs from a hostile subnet is a losing game.** Identify the parent CIDR range with whois or by sorting your access log, then block the whole range at the highest layer you control (Cloudflare WAF > server firewall > .htaccess). .htaccess blocks still let the attack hit your web server, your firewall, your PHP. Edge blocks save you the entire request.
I was looking at a client's server because something kept driving up CPU at random hours. The Nginx access log told the story in about thirty seconds: thousands of POST requests hammering `/xmlrpc.php` from a stream of different IPs that were all suspiciously close together. `45.83.64.12`, `45.83.64.47`, `45.83.64.91`, `45.83.65.13`, `45.83.65.156`. The first three octets were always 45.83.64 or 45.83.65 or 45.83.66. The same hostile network, just shuffling the last octet to evade per-IP rate limiting and fail2ban.
Adding individual IPs to fail2ban was useless. By the time I noticed the new ones in the log and added them, the attacker had cycled to a fresh batch. I needed to stop blocking IPs and start blocking ranges.
Finding the actual range
The fastest way to figure out what range an attacker is using is to look it up in WHOIS. Pick any one of the offending IPs and ask:
whois 45.83.64.12 | grep -i 'cidr\|netname\|country'On this attacker the result was something like `CIDR: 45.83.64.0/22`, `netname: SOMECHEAPHOST-NET`, `country: NL`. That CIDR is the key piece. `45.83.64.0/22` covers 1,024 addresses (45.83.64.0 through 45.83.67.255) — the entire block the attacker was randomly drawing from.
If WHOIS doesn't give you a clean answer (some smaller blocks aren't documented), the other approach is to sort your access log and look for the densest cluster yourself:
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -30Then eyeball the top 30 IPs for shared prefixes. Anything where you can see `45.83.64.x`, `45.83.65.x`, `45.83.66.x` all showing up means the parent range is somewhere around `45.83.64.0/22`.
Where should you actually block it?
There are at least four places you could put the block. Each has different tradeoffs:
- **Cloudflare WAF (or any edge CDN).** Best option if you have it. The block happens before the request ever reaches your server. Costs you zero CPU, zero bandwidth, zero log noise.
- **Server firewall (iptables, nftables, UFW).** Second best. The kernel drops the packets before they get to Nginx or PHP. Cheap, but the request still uses inbound bandwidth and the kernel still has to handle it briefly.
- **Nginx itself (`deny` in a server block).** Nginx will return 403 instantly without invoking PHP. Better than nothing but you're still using a worker, even briefly.
- **.htaccess (Apache only) or PHP-level blocking.** The worst option. The attack has already touched your filesystem, your PHP interpreter, and possibly your application bootstrap. You're still spending CPU on the attacker.
If you have Cloudflare in front of your site, do it there. The dashboard makes this trivially easy: `Security → WAF → Tools → IP Access Rules`, paste `45.83.64.0/22`, action Block, scope This Website. The whole subnet is blackholed at every Cloudflare edge globally. Your server never knows the attacker exists.
If you don't have a CDN, do it at the firewall. With UFW:
ufw insert 1 deny from 45.83.64.0/22The `insert 1` puts it at the top of the rules list, before any allow rules. With raw iptables it's:
iptables -I INPUT -s 45.83.64.0/22 -j DROPBoth of these work but neither survives a reboot unless you save them. UFW does it automatically; iptables you have to use `iptables-save > /etc/iptables/rules.v4` or use `iptables-persistent`.
When to use a wider net
Sometimes you'll see attacks coming from multiple adjacent subnets that all belong to the same hosting provider. Same hostile pattern, same user-agent, completely different /24s. At that point the question becomes: how much of this provider's address space am I willing to blackhole?
Cheap-VPS providers like the ones often used for botnets generally don't have many legitimate customers, so blocking a `/16` (65,536 addresses) or even an entire ASN is usually safe. For a major cloud (AWS, Google Cloud, Azure) you should never blanket-block — there's far too much legitimate traffic mixed in. In between, you have to make a judgment call.
I lean toward blocking aggressively when it's a hosting provider that's clearly not selling to consumers and where every request from their range to my client's site is malicious in practice. Those providers get unblocked again about as often as I find a $20 bill in the street.
Why .htaccess blocking should be a last resort
I've seen WordPress security plugins suggest dropping IPs into `.htaccess` as a way to block attackers. It works in the literal sense — Apache will return 403 to those addresses — but the attacker has already paid for nothing and you've already paid for the request handling. On a busy server under attack, every blocked-but-still-processed request is one less worker for your real users. That's exactly what the attacker wanted. Move the block up the stack until they're paying and you're not.

