Block Traffic by Country on Linux: GeoIP, iptables, nftables | INTROSERV
EUR
european

EUR

usa

USD

English En
Ex. VAT Ex. VAT 0%

How to Block Traffic by Country on a Linux Firewall Using GeoIP Rules

Level: Professional
Estimated time: ~35 minutes
Goal: Configure geoip firewall rules on a Linux server to restrict access by country, using iptables with the xtables-geoip module or nftables with IP sets - and automate updates so the lists stay current.

Introduction

If your services target a specific region, there is no reason to accept traffic from the rest of the world. Linux firewall geo blocking lets you drop packets by source country before they reach your application - cutting brute-force noise and reducing attack surface. In this tutorial, you will block traffic by country on a Linux firewall using geoip firewall rules: first with iptables and xtables-addons, then with nftables and CIDR-based IP sets, including automated database updates to keep your ip blocking by country firewall accurate.

Prerequisites

Before you begin, make sure the following conditions are met:

  • Operating system: Ubuntu 22.04/24.04 LTS, Debian 12/13, AlmaLinux/RHEL 9/10
  • Access: sudo or root access to the server
  • Firewall: iptables 1.8+ with xtables-addons, or nftables 1.0+
  • Required packages: curl, iptables, linux-headers-$(uname -r) (Debian/Ubuntu); EPEL repository enabled (RHEL/AlmaLinux 9)

Info

On Debian 12+, Ubuntu 22.04+, and RHEL/AlmaLinux 9+, the iptables command is a wrapper around the nftables backend (iptables-nft). The -m geoip match works correctly with this backend.

  • Network: a clear understanding of which countries your services need to accept traffic from
  • Required knowledge: confident use of the Linux command line, basic firewall concepts, and file editing
  • Backup: a working out-of-band console (IPMI/KVM) or provider rescue mode in case a rule locks you out

Warning

Applying country-level firewall rules over an SSH session is inherently risky. If you block your own country by mistake, you lose access. Always have an out-of-band fallback ready, and test rules before making them permanent.

Step 1: Identify your allowed and blocked countries

Before touching the firewall, decide on your policy. There are two approaches:

  • Allowlist (recommended): Allow only the countries you need, drop everything else. This is stricter and more secure.
  • Blocklist: Allow everything, drop specific countries. Simpler but leaves more surface area.

For this tutorial, we will use the allowlist approach - it aligns with the principle of least privilege. Suppose your services only serve users from the US, Germany, and Poland. The two-letter ISO country codes are: US, DE, PL.

Info

Country codes follow the ISO 3166-1 alpha-2 standard. Find the full list at https://www.iso.org/obp/ui/#search/code/. Be precise - a typo here silently blocks legitimate traffic.

Step 2: Set up GeoIP with iptables (xtables-addons)

This method uses the xt_geoip module from xtables-addons, which adds the -m geoip match to iptables. This is one of the most established ways to block ip range by country on linux.

2.1 Install xtables-addons and dependencies

Debian/Ubuntu:

sudo apt install -y linux-headers-$(uname -r) sudo apt update && sudo apt install -y \ xtables-addons-common libtext-csv-xs-perl unzip iptables curl

Warning

xtables-addons is not available in AlmaLinux 10 repositories. Use the nftables method (Step 3) instead, or build xtables-addons from source.

After installation, verify the module is available:

modinfo xt_geoip

Expected output includes a line such as:

filename: /lib/modules/.../xt_geoip.ko description: Xtables: country matching via GeoIP

If modinfo returns an error, the module did not install correctly. Check that your kernel headers match the running kernel version.

2.2 Download and build the GeoIP database

The xt_geoip module requires a local database of country-to-IP mappings. These ship as CSV files and must be converted to a binary format.

Create the working directory and the database directory:

sudo mkdir -p /usr/share/xt_geoip

Download the latest GeoIP CSV data. The upstream source provides the data through the xtables-addons tooling:

cd /tmp /usr/libexec/xtables-addons/xt_geoip_dl

Convert the downloaded CSV to binary format:

/usr/libexec/xtables-addons/xt_geoip_build -D /usr/share/xt_geoip *.csv

Expected output - a list of country codes being processed:

4540 IPv4 ranges for ZA 1071 IPv6 ranges for ZA 147 IPv4 ranges for ZW 94 IPv6 ranges for ZW ...

2.3 Apply iptables rules with GeoIP matching

Now create the actual geoip firewall rules. The following script allows traffic from your chosen countries and drops everything else on the INPUT chain.

Back up your current rules first:

sudo iptables-save > /tmp/iptables-backup-$(date +%Y%m%d).rules

Apply the country allowlist:

# Allow loopback sudo iptables -A INPUT -i lo -j ACCEPT # Allow established and related connections sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # Allow traffic from US, DE, PL only sudo iptables -A INPUT -m geoip --src-cc US,DE,PL -j ACCEPT # Log dropped packets sudo iptables -A INPUT -j LOG --log-prefix "GEOIP-DROP: " --log-level 4 # Drop everything else sudo iptables -A INPUT -j DROP

Warning

The final DROP rule blocks all traffic not matching the allowlist. Make sure your SSH source country is included, or you will be locked out immediately. If you are unsure, add a temporary ACCEPT rule for your IP before the DROP: sudo iptables -I INPUT -s <YOUR_IP_ADDRESS> -j ACCEPT.

Verify the rules are active:

sudo iptables -L INPUT -v --line-numbers

Expected output:

Chain INPUT (policy ACCEPT) num target prot opt source destination 1 ACCEPT all -- anywhere anywhere /* loopback */ 2 ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED 3 ACCEPT all -- anywhere anywhere -m geoip --source-country US,DE,PL 4 LOG all -- anywhere anywhere LOG level warning prefix "GEOIP-DROP: " 5 DROP all -- anywhere anywhere

2.4 Persist iptables rules across reboots

Rules applied with iptables are lost on reboot. Save them:

Debian/Ubuntu:

sudo apt install iptables-persistent -y sudo netfilter-persistent save

AlmaLinux/RHEL:

sudo service iptables save

Step 3: Set up GeoIP with nftables (IP sets)

If your system uses nftables (the default on Debian 11+, Ubuntu 22.04+, and RHEL 9+), you can achieve the same result using IP sets populated with country-level CIDR blocks. This approach does not require kernel modules beyond what nftables already provides.

3.1 Obtain country IP ranges

Download CIDR lists for the countries you want to allow. Several public sources provide these, such as ipdeny.com:

sudo mkdir -p /etc/nftables/geoip cd /etc/nftables/geoip # Download CIDR blocks for US, DE, PL for CC in us de pl; do sudo curl -sS -o /etc/nftables/geoip/${CC}.zone \ "https://www.ipdeny.com/ipblocks/data/aggregated/${CC}-aggregated.zone"; done

Verify the files contain CIDR ranges:

head -5 /etc/nftables/geoip/us.zone

Expected output (CIDR ranges, one per line):

1.178.0.0/23 1.178.4.0/22 1.178.8.0/21 ...

3.2 Build and load the nftables configuration

Create a script that reads the CIDR files and generates an nftables set. Save it as /etc/nftables/geoip-update.sh:

sudo nano /etc/nftables/geoip-update.sh

Paste the following:

#!/bin/bash # Generate nftables set from country CIDR files set -euo pipefail GEOIP_DIR="/etc/nftables/geoip" OUTPUT="/etc/nftables/geoip-sets.nft" echo "define ALLOWED_COUNTRIES = {" > "$OUTPUT" for zone_file in "$GEOIP_DIR"/*.zone; do while IFS= read -r cidr; do && continue echo " ${cidr}," >> "$OUTPUT" done < "$zone_file" done echo "}" >> "$OUTPUT" echo "GeoIP set generated: $(wc -l < "$OUTPUT") lines"

Make it executable:

sudo chmod +x /etc/nftables/geoip-update.sh

Run the script:

sudo /etc/nftables/geoip-update.sh

Now reference this set in your nftables configuration:

# Debian/Ubuntu: sudo nano /etc/nftables.conf # AlmaLinux/RHEL: sudo nano /etc/sysconfig/nftables.conf

Add the include and use the set in your input chain:

#!/usr/sbin/nft -f flush ruleset include "/etc/nftables/geoip-sets.nft" table inet filter { chain input { type filter hook input priority 0; policy drop; ct state invalid drop iif lo accept ct state established,related accept # Allow ICMP ip protocol icmp accept ip6 nexthdr icmpv6 accept # GeoIP: allow only listed countries ip saddr $ALLOWED_COUNTRIES accept # Log dropped packets log prefix "geoip-drop: " level info # Everything else is dropped by policy } chain forward { type filter hook forward priority 0; policy drop; } chain output { type filter hook output priority 0; policy accept; } }

Validate the syntax before applying:

sudo nft -c -f /etc/nftables.conf

If no errors appear, apply and verify:

sudo nft -f /etc/nftables.conf sudo systemctl enable --now nftables sudo nft list ruleset | head -20

Tip

For very large IP sets (100K+ entries), nftables handles them significantly better than iptables. The set-based matching in nftables uses hash maps internally, so lookup performance stays constant regardless of set size.

Step 4: Automate GeoIP database updates

IP-to-country mappings change constantly. A database from last month will have gaps. Schedule automatic updates with cron.

4.1 Create the update script

Create /usr/local/sbin/geoip-refresh.sh:

sudo nano /usr/local/sbin/geoip-refresh.sh

For iptables (xtables-addons):

#!/bin/bash set -euo pipefail cd /tmp /usr/libexec/xtables-addons/xt_geoip_dl /usr/libexec/xtables-addons/xt_geoip_build \ -D /usr/share/xt_geoip *.csv rm -f /tmp/*.csv /tmp/*.zip logger "GeoIP database updated successfully"

For nftables:

#!/bin/bash set -euo pipefail GEOIP_DIR="/etc/nftables/geoip" for CC in us de pl; do curl -sS -o "${GEOIP_DIR}/${CC}.zone" \ "https://www.ipdeny.com/ipblocks/data/aggregated/${CC}-aggregated.zone" done /etc/nftables/geoip-update.sh systemctl reload nftables logger "GeoIP nftables sets updated and reloaded"

Make it executable:

sudo chmod +x /usr/local/sbin/geoip-refresh.sh

4.2 Schedule with cron

Before scheduling the cron task, ensure the nftables service is enabled and running (if using the nftables method):

sudo systemctl enable --now nftables

Run the update weekly:

sudo crontab -e

Add:

0 3 * * 0 /usr/local/sbin/geoip-refresh.sh >> /var/log/geoip-update.log 2>&1

This runs every Sunday at 3:00 AM. After the first scheduled run, verify it worked:

cat /var/log/geoip-update.log

Step 5: Verification and testing

5.1 Test from an allowed country

From a machine in one of your allowed countries, connect via SSH or make an HTTP request:

curl -v http://<YOUR_SERVER_IP>

The connection should succeed normally.

5.2 Simulate a blocked country

Use curl with a known IP from a blocked country through a proxy, or test locally by temporarily removing your own country from the allowlist and attempting a connection from a second terminal.

Info

Online services like https://ipinfo.io let you verify which country a given IP is registered to. Use curl https://ipinfo.io/<YOUR_IP_ADDRESS> to confirm.

5.3 Check firewall counters

iptables:

sudo iptables -L INPUT -v -n

Look at the packet counters on the DROP rule - they should be incrementing as blocked traffic arrives.

nftables:

sudo nft list chain inet filter input

5.4 Monitor system logs

Check for dropped packets in the system log:

sudo journalctl -k --since "1 hour ago" | grep -i "geoip-drop"

Warning

If you observe a high volume of kernel log messages about dropped packets and your system becomes unresponsive, a logging flood may be the cause. In the worst case, this can trigger a kernel panic. If you have configured crashkernel parameter and the kdump service, a kernel panic will produce a crash dump (vmcore) in /var/crash that you can analyze later. Without kdump, you only get a reboot and no diagnostics. See the section below for a brief overview.

Step 6: Protecting against kernel panic during firewall changes

Aggressive firewall changes on a busy production server - especially those that cause sudden traffic spikes or logging floods - can in rare cases lead to a kernel panic. If that happens and you have not prepared, you get nothing: just a reboot and no data about what went wrong.

Kdump is the standard Linux mechanism for capturing a crash dump when a kernel panic occurs. It uses kexec to boot a secondary capture kernel, which writes the memory image (vmcore) to disk before the system restarts.

To ensure you have diagnostics available:

  1. Verify kdump is installed and enabled. On RHEL/AlmaLinux, it is typically pre-installed. On Debian/Ubuntu:

    sudo apt install kdump-tools kexec-tools -y

  2. Check that the crashkernel parameter is set in your bootloader configuration:

    cat /proc/cmdline | grep crashkernel

    You should see something like crashkernel=256M. If it is missing, you need to configure crashkernel parameter in GRUB and reboot. The exact amount to reserve memory for kdump linux depends on your total RAM - 256M is a safe default for servers with 4 GB or more.

  3. Verify the kdump service is active:

    sudo systemctl status kdump

Info

If a kernel panic does occur, the system will use kexec to boot the capture kernel, write the vmcore to /var/crash, and then reboot normally. You can then perform vmcore analysis linux using the crash utility to determine the root cause. This is a standard part of linux kernel panic troubleshooting on production systems. For a complete guide on how to enable kdump in linux and kexec kdump linux configuration, refer to a dedicated kdump tutorial.

Troubleshooting

  • Locked out via SSH: Use out-of-band console / rescue mode.
  • modinfo xt_geoip: not found: Install linux-headers-$(uname -r).
  • iptables: command not found (Debian 13): Install iptables with sudo apt install iptables.
  • nftables.service is not active: Run sudo systemctl enable --now nftables.
  • Empty /etc/sysconfig/nftables.conf on AlmaLinux: Use this path instead of /etc/nftables.conf to configure rules.

Rollback

To undo country-based blocking and restore open access:

iptables - restore from backup:

sudo iptables-restore < /tmp/iptables-backup-*.rules

Info

If you applied GeoIP rules on a server with no prior firewall configuration, the backup file will be empty. In that case, use the flush-and-policy commands below instead of iptables-restore.

Or flush all rules:

sudo iptables -F INPUT sudo iptables -P INPUT ACCEPT

Warning

Flushing to ACCEPT removes all firewall protection. Apply your standard security ruleset immediately after.

nftables - restore default:

sudo nft flush ruleset

Then restore your base nftables configuration (without GeoIP):

sudo nft -f /etc/nftables.conf.backup

To remove the GeoIP components entirely:

sudo rm -rf /etc/nftables/geoip sudo rm -f /etc/nftables/geoip-sets.nft /etc/nftables/geoip-update.sh sudo crontab -l | grep -v geoip-refresh | sudo crontab -

Remove xtables-addons (if installed):

Debian/Ubuntu:

sudo apt remove xtables-addons-common -y

AlmaLinux/RHEL:

sudo dnf remove xtables-addons -y

Conclusion

Your server now uses geoip firewall rules to block traffic by country on linux - either through iptables with xt_geoip or nftables with CIDR-based IP sets. The database updates automatically, you have a rollback plan, and with kdump configured for crash dump capture in case of a kernel panic, your production environment is covered from both routine threats and worst-case scenarios.

Document Version: 1.0
Last Updated: May 2026
Owner: Technical Documentation Team

VAT

  • Other

    Ex. VAT

    0%
  • austria

    Austria

    20%
  • Belgium

    Belgium

    21%
  • Bulgaria

    Bulgaria

    20%
  • Croatia

    Croatia

    25%
  • Cyprus

    Cyprus

    19%
  • Czech Republic

    Czech Republic

    21%
  • Denmark

    Denmark

    25%
  • Estonia

    Estonia

    22%
  • France

    France

    20%
  • Finland

    Finland

    24%
  • Germany

    Germany

    19%
  • Greece

    Greece

    24%
  • Hungary

    Hungary

    27%
  • Ireland

    Ireland

    23%
  • Italy

    Italy

    22%
  • Latvia

    Latvia

    21%
  • Lithuania

    Lithuania

    21%
  • Luxembourg

    Luxembourg

    17%
  • Malta

    Malta

    18%
  • Netherlands

    Netherlands

    21%
  • Poland

    Poland

    23%
  • Portugal

    Portugal

    23%
  • Romania

    Romania

    19%
  • Slovakia

    Slovakia

    20%
  • Slovenia

    Slovenia

    22%
  • Spain

    Spain

    21%
  • Sweden

    Sweden

    25%
  • USA

    USA

    0%
european
states
  • germany
  • Español
  • Italiano
  • Poland
  • Русский
  • Slovenski
  • Türkçe
  • ukraine
  • kingdom
  • French
  • Hrvatska
  • Other
  • Austria
  • Belgium
  • Bulgaria
  • Croatia
  • Cyprus
  • Czech Republic
  • Denmark
  • Estonia
  • Finland
  • France
  • Germany
  • Greece
  • Hungary
  • Ireland
  • Italy
  • Latvia
  • Lithuania
  • Luxembourg
  • Malta
  • Netherlands
  • Poland
  • Portugal
  • Romania
  • Slovakia
  • Slovenia
  • Spain
  • Sweden
  • USA