If your access logs are full of login attempts, comment spam, and card-testing traffic from countries you've never shipped to, blocking by country is often the fastest way to cut noise before you touch anything else on the server. Here's how to do it properly - at the firewall on a VPS, and in cPanel/CSF or Cloudflare if you're on shared or managed hosting - without accidentally locking out real customers or search engines.
Symptom: what country-based attack traffic looks like
You don't usually go looking for this - you find it while chasing something else. A few common tells:
- WHM/cPanel's
cphulk_failed_loginlog or/var/log/secureis full of failed SSH or webmail logins from IP ranges that geolocate to countries you have no business relationship with. - WooCommerce or a payment gateway shows a spike in declined transactions from the same handful of countries, all within minutes of each other - classic card-testing behavior.
- Google Analytics or your access log shows a traffic spike with a 100% bounce rate and zero time on page, all hitting
/wp-login.phpor/xmlrpc.php. - Your contact form or comments section is drowning in spam submitted in a script, not typed by a person, again clustered by source country.
None of this means "block every visitor from that country forever." It means: that traffic pattern isn't real customers, so filtering it at the edge saves you CPU, log noise, and a few 2 a.m. alerts.
Cause: no geographic filtering at any layer
By default, nothing in a standard LAMP or LEMP stack cares where a request comes from. Apache, Nginx, PHP-FPM, and MySQL will happily process a request from anywhere the packet can reach - which is everywhere, since the whole point of hosting a public website is that it's public. Geo-blocking has to be added deliberately, at one of three places:
- The firewall (CSF, UFW, iptables) - drops the connection before it ever reaches Apache/Nginx.
- The reverse proxy / CDN (Cloudflare) - drops or challenges the request before it reaches your server at all.
- The application (a WordPress plugin, a PHP check against a GeoIP database) - the request still hits your server and uses PHP/MySQL resources before being blocked, so it's the least efficient option and the one to avoid if you have a choice.
The rule of thumb: block as far upstream as you can. A request dropped at Cloudflare's edge never touches your server. A request dropped by CSF at the kernel firewall level never wakes up Apache. A request blocked by a WordPress plugin has already cost you a full PHP bootstrap and often a database connection - it works, but it's the most expensive way to do it.
Fix 1: Cloudflare (if your domain is already proxied through it)
This is the easiest and cheapest option if you're already using Cloudflare's free plan for DNS or CDN, and it's usually where you should start.
- Log into the Cloudflare dashboard and select your domain.
- Go to Security → WAF → Custom rules (older dashboards: Firewall Rules).
- Create a rule with an expression like:
(ip.geoip.country eq "CN" or ip.geoip.country eq "RU" or ip.geoip.country eq "KP") - Set the action. Don't jump straight to Block on a new rule - start with Managed Challenge for a week and watch the Security Events log. A real visitor passes a challenge in a couple of seconds; a bot usually doesn't bother trying.
- Once you've confirmed the country codes are actually the source of junk traffic and not, say, a legitimate reseller or dev team working from there, switch the action to Block.
Cloudflare's free plan gives you 5 custom rules and country-level targeting is included - you don't need a paid plan for basic geo-blocking.
Fix 2: CSF on a cPanel/WHM VPS
If you're running your own VPS with WHM and ConfigServer Security & Firewall (CSF), country blocking is built in and doesn't need a CDN in front of you.
- In WHM, go to Plugins → ConfigServer Security & Firewall.
- Click Firewall Configuration and find
CC_DENY(deny by country code) andCC_ALLOW(allow-list, use only if you want to lock a service down to specific countries). - Enter the two-letter ISO country codes you want blocked, comma-separated, e.g.:
CC_DENY = "CN,RU,KP" - Scroll down and confirm
CC_DENY_SMTPis set separately if you also want to block those countries from connecting to your mail ports - it's a different setting from the general firewall deny. - Save, then restart CSF from the WHM interface (or
csf -rover SSH). CSF downloads the GeoIP country database automatically on first use and refreshes it periodically via cron.
Check that the country database actually downloaded before you trust the rule:
ls -la /var/lib/csf/*.zone 2>/dev/null
csf -g 1.2.3.4 # look up what country an IP resolves toIf csf -g comes back empty for a known IP, the GeoIP database didn't download - usually an outbound connectivity issue on the VPS, not a config mistake. Check /var/log/lfd.log for download errors.
Fix 3: UFW/iptables on a bare VPS (no CSF)
Without CSF, country blocking at the firewall means pulling IP ranges yourself and loading them into iptables with ipset, since UFW has no native geo-blocking. It's more manual but works on any distro:
# install ipset and download a country's CIDR list (example: source from ipdeny.com)
apt install ipset -y
curl -s https://www.ipdeny.com/ipblocks/data/countries/cn.zone -o /tmp/cn.zone
ipset create cn-block hash:net
while read -r ip; do ipset add cn-block "$ip"; done < /tmp/cn.zone
iptables -I INPUT -m set --match-set cn-block src -j DROPMake it persistent with iptables-persistent or a cron job that re-downloads and reloads the set weekly, since these lists change as ranges get reallocated. This approach is fiddlier than CSF and easy to get wrong, so if you're already paying for cPanel/WHM, use CC_DENY instead - it's the same underlying idea with far less maintenance.
What NOT to block, and why this bites people
This is the part that generates support tickets, so read it before you save the rule:
- Don't block by country at the application layer only, and forget the API. If you geo-block the website but not your REST API or webhook endpoints, attackers just hit those directly.
- Don't block countries where your payment processor, CDN, or monitoring service has infrastructure. Stripe, Cloudflare, UptimeRobot, and similar services route through data centers in dozens of countries - block too aggressively and your own uptime checks or webhook callbacks start failing, and it looks like a server outage when it's actually your own firewall rule.
- Don't forget Googlebot and other crawlers. Googlebot primarily crawls from US IP ranges, so blocking the US isn't realistic for most sites - but if you're geo-blocking anything close to your own hosting region, verify your SEO tools and site monitors can still reach you.
- Don't use this as your only security layer. Geo-blocking cuts volume, it doesn't stop a determined attacker using a VPN or proxy exiting from an allowed country. Pair it with fail2ban/cPHulk for brute-force attempts and a WAF rule set (ModSecurity, Sucuri) for anything that gets through.
Prevention: review the list periodically
A country block list isn't "set and forget." Attack sources shift, IP ranges get reassigned, and a country that was quiet six months ago can become the top source of junk traffic next quarter. Put a recurring reminder - quarterly is enough for most sites - to pull up your CSF lfd logs or Cloudflare Security Events and check whether the current list still matches where the actual bad traffic is coming from. It's also worth checking your real analytics for legitimate traffic from any country you're about to block, before you block it - a five-minute check beats an angry email from a customer who suddenly can't reach your site.
Quick reference: symptom → cause → fix
| Symptom | Likely cause | Fix |
|---|---|---|
| Repeated failed SSH/WHM logins from one region | No firewall-level geo filter | CSF CC_DENY or ipset+iptables block |
| Spam form submissions clustered by country | No edge filtering before requests reach WordPress | Cloudflare custom rule (Managed Challenge, then Block) |
| Card-testing / fraud attempts from specific countries | Payment form has no geographic gate | Block at Cloudflare edge, not just in the gateway's fraud rules |
| Uptime monitor or webhook suddenly failing after adding a block | Blocked a country your CDN/monitoring provider uses | Whitelist that service's known IP ranges before re-enabling the block |
CSF CC_DENY set but nothing is actually blocked | GeoIP country database never downloaded | Check /var/log/lfd.log, confirm outbound connectivity, run csf -g <ip> |