iptables rulesets can grow to thousands of entries, making them impossible to manage manually.
Let’s watch some traffic get blocked by iptables.
Imagine a busy web server. It’s getting hammered by requests, some legitimate, some not. We want to allow traffic on ports 80 and 443, but block everything else. We also want to block a specific IP address that’s been sending us garbage.
# Allow incoming SSH
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Allow incoming HTTP
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
# Allow incoming HTTPS
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Block a specific IP address
iptables -A INPUT -s 192.168.1.100 -j DROP
# Drop all other incoming traffic
iptables -A INPUT -j DROP
When a packet arrives, iptables checks it against these rules, in order. If a packet matches a rule, it performs the specified action (ACCEPT, DROP, REJECT, etc.) and stops processing further rules for that packet. If it reaches the end without a match, the default policy for the chain applies. In this case, our final DROP rule is a catch-all.
The problem with this approach at scale is that the rule list becomes unwieldy. Adding or removing IPs, or changing port allowances, means reordering or inserting into a massive, hard-to-read list. This is where managing allow and block lists becomes critical.
The core idea is to separate the "list" of IPs or ports from the "policy" that uses them. This is typically achieved using iptables’ "set" match extensions.
First, we need to create the sets. These are in-memory hash tables that iptables can query very efficiently.
# Create a set for allowed IPs (e.g., internal network)
ipset create allowed_ips hash:ip
# Add some IPs to the allowed set
ipset add allowed_ips 10.0.0.0/8
ipset add allowed_ips 172.16.0.0/12
ipset add allowed_ips 192.168.0.0/16
# Create a set for blocked IPs
ipset create blocked_ips hash:ip
# Add a specific malicious IP to the blocked set
ipset add blocked_ips 1.2.3.4
Now, we can write iptables rules that reference these sets. This is where the magic happens for scale.
# Allow traffic from IPs in the allowed_ips set
iptables -A INPUT -m set --match-set allowed_ips src -j ACCEPT
# Block traffic from IPs in the blocked_ips set
iptables -A INPUT -m set --match-set blocked_ips src -j DROP
# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow SSH (if not already covered by allowed_ips)
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Drop all other traffic
iptables -A INPUT -j DROP
The key advantage here is that managing the list of IPs is now done with ipset commands, which are much faster and cleaner than iptables commands for bulk operations. You can add or remove thousands of IPs from allowed_ips or blocked_ips without touching the iptables ruleset itself. The iptables rules just say "if the source IP is in this set, do this."
When an ipset set is created with hash:ip, it uses a hash table data structure. Lookups in a hash table are, on average, O(1) – constant time. This means that adding or checking for the existence of an IP address takes roughly the same amount of time, regardless of whether the set contains 10 IPs or 10 million. This is the fundamental reason why ipset scales so much better than a long list of individual iptables rules. The underlying system for ipset is designed for efficient membership testing.
Consider the scenario where you need to block a new IP. Instead of iptables -I INPUT 5 -s 1.2.3.5 -j DROP (inserting it at a specific line number), you simply run ipset add blocked_ips 1.2.3.5. The iptables rule -m set --match-set blocked_ips src -j DROP automatically picks up this change.
You can also use hash:net for CIDR blocks, hash:port for ports, and hash:ip,port for combinations, further enhancing flexibility. For example, to block a specific IP from accessing only SSH:
# Create a set for IPs blocked from SSH
ipset create blocked_ssh hash:ip
# Add an IP to this set
ipset add blocked_ssh 1.2.3.6
# Add a rule to block this IP from SSH
iptables -A INPUT -p tcp --dport 22 -m set --match-set blocked_ssh src -j DROP
The truly powerful aspect, often overlooked, is the ability to dynamically change which sets are used by iptables rules without restarting the firewall. For instance, if you have a VIP (Virtual IP) that needs to temporarily stop receiving traffic, you can remove all IPs from its associated ipset and then add them back later. The iptables rules remain untouched, but the traffic flow changes instantly. This is invaluable for high-availability scenarios or during maintenance windows where you need fine-grained control without service interruption.
The next hurdle you’ll face is managing these ipset lists across reboots and ensuring they are populated correctly on system startup.