Why I Needed Geo-Blocking for My Services
I run several self-hosted services on my Proxmox setup—SSH access to my servers, web apps behind reverse proxies, and a few internal tools exposed through Cloudflare tunnels. While I’ve always used strong passwords and SSH keys, I kept seeing authentication attempts from IP ranges I’d never interact with. Most were automated scans from regions where I have no users, no collaborators, and no reason to accept connections.
The problem wasn’t an immediate breach, but unnecessary noise. Every failed login attempt is still a log entry, a potential attack vector, and a waste of resources. I wanted a way to drop traffic before it even reached my services—not just block it at the application layer, but reject it at the firewall level.
That’s when I started looking into nftables-based geo-blocking using ipset and automated GeoIP database updates. I’d already migrated from iptables to nftables on my Debian-based systems, so this was a natural next step.
My Real Setup
Here’s what I’m working with:
- Proxmox VE 8.x running multiple LXC containers and VMs
- Debian 12 containers handling SSH access, web services, and reverse proxies
- nftables already configured for basic firewall rules (I migrated from iptables about a year ago)
- MaxMind GeoLite2 database for IP-to-country mapping (free tier, updated weekly)
- A simple cron job to pull fresh GeoIP data and regenerate ipset rules
I don’t use a commercial GeoIP service. MaxMind’s free database is accurate enough for my needs—I’m not trying to catch every edge case, just block the bulk of unwanted traffic.
How I Implemented It
Step 1: Getting the MaxMind GeoIP Database
I signed up for a free MaxMind account and downloaded the GeoLite2-Country database in CSV format. MaxMind also offers an API for automated downloads, which I use in my cron job.
The database gives me IP ranges mapped to country codes. I only care about a few countries where I actually operate—everything else gets dropped.
Step 2: Converting GeoIP Data to nftables Sets
MaxMind’s CSV files aren’t directly usable in nftables. I wrote a small Python script to parse the data and generate nftables set definitions. The script:
- Reads the GeoLite2-Country-Blocks-IPv4.csv and GeoLite2-Country-Blocks-IPv6.csv files
- Filters IP ranges for allowed countries (in my case, a short whitelist)
- Outputs nftables-compatible set definitions to /etc/nftables/geoip.nft
The output looks like this:
define allowed_ipv4 = {
1.2.3.0/24,
5.6.7.0/24,
...
}
define allowed_ipv6 = {
2001:db8::/32,
...
}
I store this in a separate file and include it in my main nftables.conf.
Step 3: Configuring nftables Rules
My /etc/nftables.conf includes the generated GeoIP sets and applies them to SSH and web service ports:
#!/usr/sbin/nft -f
flush ruleset
include "/etc/nftables/geoip.nft"
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
# Allow loopback
iif lo accept
# Allow established connections
ct state established,related accept
# Allow SSH only from allowed countries
ip saddr $allowed_ipv4 tcp dport 22 accept
ip6 saddr $allowed_ipv6 tcp dport 22 accept
# Allow HTTP/HTTPS only from allowed countries
ip saddr $allowed_ipv4 tcp dport { 80, 443 } accept
ip6 saddr $allowed_ipv6 tcp dport { 80, 443 } accept
# Drop everything else
drop
}
}
I set the default policy to drop, so anything not explicitly allowed is rejected. This is stricter than I used to run, but it’s worth it for the peace of mind.
Step 4: Automating GeoIP Updates
MaxMind updates their database weekly. I set up a cron job to:
- Download the latest GeoLite2 database using MaxMind’s API
- Run my Python script to regenerate /etc/nftables/geoip.nft
- Reload nftables with nft -f /etc/nftables.conf
The cron job runs every Monday at 3 AM:
0 3 * * 1 /usr/local/bin/update-geoip.sh
The script checks if the new database differs from the current one before reloading nftables. No point in restarting the firewall if nothing changed.
What Worked
The setup has been running for about four months now, and it’s doing exactly what I wanted:
- SSH login attempts from blocked regions dropped by over 90%. My logs are cleaner, and I’m not wasting CPU cycles on authentication checks for traffic I’ll never accept.
- Web service access is now limited to countries where I actually have users. I can still use Cloudflare tunnels for remote access when I’m traveling, but direct IP connections are restricted.
- nftables performance is solid. I was worried about the overhead of checking every packet against large IP sets, but I haven’t noticed any latency or CPU spikes.
- The automated updates work reliably. I check the logs occasionally, and the GeoIP data refreshes every week without manual intervention.
What Didn’t Work
Not everything went smoothly:
- My first attempt used the MaxMind binary database (mmdb format) with a third-party tool to convert it to nftables sets. The tool was slow and occasionally crashed. I switched to parsing the CSV files directly, which is faster and more reliable.
- I initially tried to block all countries except a whitelist, but MaxMind’s database doesn’t cover every IP range. Some legitimate traffic from cloud providers (AWS, Azure, etc.) wasn’t mapped to a country and got dropped. I had to add manual exceptions for those ranges.
- IPv6 support was trickier than I expected. Some ISPs assign dynamic IPv6 prefixes, and MaxMind’s database doesn’t always keep up. I had to be more lenient with IPv6 rules to avoid locking myself out.
- The first time I reloaded nftables after a bad configuration, I locked myself out of SSH. I had to use Proxmox’s console to fix it. Now I always test new rules with a delayed revert:
nft -f /etc/nftables.conf && sleep 60 && nft -f /etc/nftables.conf.backup. If something breaks, the old rules restore automatically.
Key Takeaways
- Geo-blocking at the firewall level is effective for reducing noise, but it’s not a replacement for strong authentication. I still use SSH keys and fail2ban as additional layers.
- MaxMind’s free GeoLite2 database is good enough for most self-hosted use cases. The paid version has better accuracy, but I haven’t needed it.
- Parsing CSV files directly is simpler and more reliable than using third-party conversion tools.
- Always test firewall changes with a fallback mechanism. Getting locked out is avoidable if you plan ahead.
- IPv6 geo-blocking is less precise than IPv4. Be prepared to adjust your rules based on real-world usage.
This setup has been stable and low-maintenance. I check the logs every few weeks to make sure nothing legitimate is getting blocked, but I haven’t had to adjust the rules much since the initial configuration.