The Firewall That Kept Breaking My Deployments
Six months ago, I was managing a fleet of 12 Linux VPS instances across three different cloud providers. Each server had its own tangle of iptables rules — some copied from Stack Overflow, some inherited from previous sysadmins, and a few that nobody remembered writing. Every deployment was a gamble. A missed ip6tables rule would quietly leave IPv6 traffic unfiltered. An arptables mismatch on the KVM host would break internal routing for hours before anyone noticed.
Three years of managing VPS fleets taught me one thing: test everything before touching production. But careful testing couldn’t fix iptables’ fundamental design problem. You’re not managing one firewall — you’re juggling four separate subsystems that know nothing about each other.
Why Iptables Is Showing Its Age
Most of my firewall headaches traced back to iptables’ design — built in the Linux 2.4 era when IPv6 was still theoretical and ARP filtering was an afterthought.
Here’s what you actually need to manage with iptables on a modern server:
- iptables — IPv4 packet filtering and NAT
- ip6tables — the exact same rules, but for IPv6
- arptables — ARP-level filtering
- ebtables — Ethernet bridge filtering
A rule you write for IPv4 does nothing for IPv6. Every policy change means touching at least two separate rule sets. It’s trivially easy to apply a security fix to iptables while forgetting ip6tables entirely. I’ve seen production servers locked down tight on IPv4 while their IPv6 addresses accepted connections from anywhere.
Performance degrades predictably on busy servers. Every packet evaluation walks a linear list of rules. Hit 300+ rules — not unusual in a multi-tenant environment — and the overhead shows up in latency benchmarks. Rule reloads aren’t truly atomic either. iptables-restore gets close, but there’s a brief window during reload where traffic can slip through or drop unexpectedly.
The Alternatives: What You’re Actually Choosing Between
When I decided to fix this, I narrowed it down to three paths:
Option 1: Stay on Iptables With Better Tooling
Tools like ferm or iptables-save/restore scripts can reduce duplication. You write rules once and generate both iptables and ip6tables output. It works, but you’re still patching over a cracked foundation. The linear evaluation problem doesn’t go away, and arptables/ebtables remain separate.
Option 2: UFW or Firewalld
UFW (Ubuntu) and firewalld (RHEL/Fedora) are high-level abstractions that generate iptables rules under the hood. Fine for simple servers — open port 22, allow HTTP, block everything else. The moment you need NAT rules, custom chains, or anything beyond basic allow/deny, you’re fighting the abstraction layer. Firewalld already added a nftables backend in recent versions. That’s a hint about where it’s heading.
Option 3: Nftables — The Modern Replacement
Nftables consolidates iptables, ip6tables, arptables, and ebtables into a single framework. Rule evaluation uses a set-based data structure instead of a linear list — dramatically faster once you cross a few hundred rules. Atomic rule replacement works natively, not as an approximation. The syntax is readable enough that a fresh engineer can understand a ruleset without consulting a manual.
Merged into the kernel in 3.13 (January 2014), nftables is now the default in Debian 10+, RHEL 8+, and Fedora 32+. Every major distribution has already committed to it.
Getting Started With Nftables
On most modern distributions, nftables is already installed. Check the current state:
nft list ruleset
Empty output means an empty ruleset — you’re starting clean. Migrating from iptables? Use the translation tool:
# Convert existing iptables rules to nftables syntax
iptables-save | iptables-translate > /tmp/nftables-migrated.nft
ip6tables-save | ip6tables-translate >> /tmp/nftables-migrated.nft
# Review before applying
cat /tmp/nftables-migrated.nft
The Basic Ruleset Structure
Nftables organizes rules into tables containing chains. Unlike iptables — where tables are hardcoded as filter, nat, and mangle — nftables lets you name tables freely and choose their address family explicitly.
# Create a simple inet table (handles both IPv4 and IPv6)
nft add table inet main_firewall
# Add an input chain with default drop policy
nft add chain inet main_firewall input { type filter hook input priority 0\; policy drop\; }
# Allow established connections
nft add rule inet main_firewall input ct state established,related accept
# Allow loopback
nft add rule inet main_firewall input iif lo accept
# Allow SSH
nft add rule inet main_firewall input tcp dport 22 accept
# Allow HTTP and HTTPS
nft add rule inet main_firewall input tcp dport { 80, 443 } accept
Notice the inet family — one chain handles both IPv4 and IPv6 simultaneously. That single change eliminates the entire class of “forgot to update ip6tables” bugs.
Using a Configuration File
Individual nft add commands are fine for testing. Production rules belong in /etc/nftables.conf. The syntax is considerably cleaner than anything iptables produces:
#!/usr/sbin/nft -f
flush ruleset
table inet firewall {
# Block known bad IPs using a set — no rule duplication
set blocklist {
type ipv4_addr
flags interval
elements = { 192.168.100.0/24, 10.0.0.5 }
}
chain input {
type filter hook input priority 0; policy drop;
# Drop blocked IPs immediately
ip saddr @blocklist drop
ct state invalid drop
ct state { established, related } accept
iif "lo" accept
# ICMP — allow ping but rate limit
ip protocol icmp icmp type echo-request limit rate 10/second accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
# SSH with connection rate limiting (anti-brute-force)
tcp dport 22 ct state new limit rate 5/minute accept
# Web traffic
tcp dport { 80, 443 } accept
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
Apply it and make it persistent:
# Apply the ruleset
nft -f /etc/nftables.conf
# Enable on boot (systemd)
systemctl enable nftables
systemctl start nftables
# Verify
nft list ruleset
NAT Configuration — Where Nftables Really Shines
Masquerading and port forwarding show up constantly on servers running containers or VMs. This NAT config handles both DNAT and masquerade in one file — no parallel ip6tables command, no manual state sync between tools:
table ip nat {
chain prerouting {
type nat hook prerouting priority -100;
# Forward port 8080 on host to internal container at 172.16.0.10:80
tcp dport 8080 dnat to 172.16.0.10:80
}
chain postrouting {
type nat hook postrouting priority 100;
# Masquerade traffic going out through eth0
oif "eth0" masquerade
}
}
The Testing Protocol That Saved Me Twice
Atomic reloads are real and useful. They still won’t stop you from locking yourself out if the new policy drops all inbound traffic. My standard process before touching firewall rules on any production server:
# Save current working ruleset FIRST
nft list ruleset > /root/nft-backup-$(date +%Y%m%d-%H%M).conf
# Test syntax without applying
nft -c -f /etc/nftables.conf
# Schedule automatic rollback in case you lose access
# (Run this BEFORE applying new rules)
echo "nft flush ruleset && nft -f /root/nft-backup-*.conf" | at now + 5 minutes
# Apply new rules
nft -f /etc/nftables.conf
# Verify connectivity from another machine, THEN cancel the rollback
atrm $(atq | tail -1 | cut -f1)
The at job trick has recovered two servers for me — once after a copy-paste error dropped all SSH traffic, once after a misconfigured forward chain blocked container networking. Five minutes is enough time to notice the problem before the rollback fires.
Migrating Your Production Servers
Six months running nftables across all 12 servers changed the maintenance picture completely. Rule updates went from touching four separate files to editing one. IPv6 coverage changed from “probably fine” to actually guaranteed. Adding 50 IPs to the blocklist became a single set operation — not 50 appended rules slowing down every subsequent packet evaluation.
For most servers, the migration takes an afternoon: run iptables-translate to get a starting point, clean up the output (readable enough to review in under 10 minutes), test with the rollback timer in place, then flip. The iptables compatibility layer stays available on most distros if you need to roll further back.
Debian, RHEL, and Fedora all ship nftables as the default now. Firewalld uses it as a backend. Docker has native nftables support actively in development. The ecosystem has standardized. If you’re still hand-maintaining iptables rules in 2026, you’re carrying debt against infrastructure the major distributions have already moved past.

