Most of the attack traffic hitting a public-facing server comes from a handful of countries. Not because those countries are inherently malicious — they just have large numbers of compromised machines in botnets, or hosting providers with lax abuse policies.

We block several countries at the kernel level using xtables-addons with the xt_geoip module. Packets from blocked IP ranges are dropped before they reach Traefik, nginx, or any application. The firewall rules persist across reboots via netfilter-persistent.

Why kernel-level?

The alternative is blocking at the application level — Traefik middleware, nginx geo blocks, or fail2ban with GeoIP lookups. All of these work, but they all require the packet to traverse the network stack, get parsed by userspace, and then get dropped. That’s CPU time, memory, and log entries for traffic you never wanted in the first place.

Kernel-level iptables rules with GeoIP matching drop the packet at the netfilter stage. The TCP handshake never completes. No connection is established. No application resources are consumed.

What we also run

GeoIP blocking is the first layer. Behind it:

fail2ban watches Traefik access logs for two patterns:

  • Authentication failures (401/403 responses) — catches credential stuffing
  • Exploit path scanning (requests for wp-admin, .env, .git, phpMyAdmin, etc.) — catches automated vulnerability scanners

Both jails ban the offending IP for a configurable period. Combined with GeoIP blocking, the vast majority of malicious traffic never generates a ban because it never arrives in the first place.

The trade-off

GeoIP blocking is a blunt instrument. It blocks legitimate users in those countries. For a B2B application serving professionals in a specific geographic market, that’s an acceptable trade-off. For a public-facing consumer service, it wouldn’t be.

The key is knowing your audience. If none of your users are in the blocked regions, the cost is zero and the benefit is significant. If even one legitimate user might be affected, you need a different approach.

Maintenance

The GeoIP database needs periodic updates — IP allocations change over time. The xt_geoip module uses a CSV-based database that can be regenerated from public allocation data. It’s not something that needs daily attention, but it shouldn’t be forgotten either.