iptables is dead. Long live nftables.
Here’s the thing: iptables has been the default Linux firewall since the 90s. It’s battle-tested, widely documented, and absolutely everywhere. It’s also been deprecated since 2022, with the kernel maintainers quietly moving everyone toward nftables. If you’ve set up a Linux box in the last year and used UFW or firewalld, congratulations—you’ve already been using nftables. You just didn’t know it.
The reason nobody talks about this much is that iptables still works fine. Your rules won’t explode tomorrow. But the ecosystem has moved on, and there’s a really good reason: nftables is faster, cleaner, and does in one tool what iptables needed four separate tools to accomplish.
Let’s talk about why you should care, and more importantly, how to actually use the thing without losing your mind.
Why nftables exists
iptables did one job really well: filter packets. But Linux networking needed more than that, so we got:
- iptables — IPv4 filtering
- ip6tables — IPv6 filtering
- arptables — ARP protocol filtering
- ebtables — Ethernet bridging filtering
Four different tools. Four different syntaxes. Four different ways to accidentally shoot yourself in the foot.
nftables merges all of this into one unified system. Same syntax, same command-line tool, one ruleset evaluation engine. It’s faster because the kernel doesn’t have to switch contexts between different subsystems. It’s cleaner because you’re not juggling four different tools.
And here’s the kicker: if you’re using a modern Linux distro (anything recent), you’re probably already running nftables under the hood via UFW, firewalld, or Docker. You might as well learn the underlying layer.
The mental model shift
iptables is all about chains and built-in tables. You’ve got INPUT, OUTPUT, FORWARD chains in the filter table. You modify those chains. Done.
nftables flips the script slightly. You have:
- Tables — you create them (no built-in defaults). Each table handles one address family (inet, ip, ip6, arp, bridge)
- Chains — you attach these to your tables and define the flow (ingress, prerouting, input, forward, output, postrouting)
- Rules — the actual filtering logic
This sounds more complicated, but it’s actually a lot cleaner once you get the mental model right. Think of it like this: with iptables you’re editing predefined blueprints. With nftables, you’re building the blueprint from scratch. That sounds like more work, but you only do it once.
Your first nftables rules
Let’s start by seeing what you’ve got:
$ sudo nft list rulesetIf you’re on a fresh system, you might get nothing. That’s fine—we’re building from zero.
Here’s a practical starter ruleset that covers 80% of what people actually need:
#!/usr/bin/env nft -f
flush ruleset
table inet filter { chain input { type filter hook input priority 0; policy drop;
# Allow loopback iif lo accept
# Allow established/related connections ct state established,related accept
# Allow ICMP (ping) ip protocol icmp accept ip6 nexthdr icmpv6 accept
# Allow SSH tcp dport 22 accept
# Allow HTTP/HTTPS tcp dport { 80, 443 } accept
# Everything else gets dropped (policy is already drop, but be explicit) reject with icmp type port-unreachable }
chain output { type filter hook output priority 0; policy accept; }
chain forward { type filter hook forward priority 0; policy drop; }}Save this to /etc/nftables.conf, then load it:
$ sudo nft -f /etc/nftables.confDone. Your firewall is now active. Everything gets dropped by default except the ports you explicitly allowed. This is the opposite of iptables’ default behavior (allow everything), and it’s the right way to think about security.
The killer feature: Sets and maps
Here’s where nftables gets really good. Remember when you wanted to block an entire subnet with iptables? You’d write one rule per IP. Terrible.
nftables has sets:
table inet filter { set blacklist { type ipv4_addr elements = { 192.168.1.100, 10.0.0.0/24 } }
chain input { type filter hook input priority 0; policy drop;
ip saddr @blacklist drop
tcp dport 22 accept accept }}One rule matches against the entire set. Dynamic, efficient, way cleaner than writing five rules for a subnet.
You can even update the set without reloading the whole ruleset:
$ sudo nft add element inet filter blacklist { 203.0.113.42 }NAT for your home router
If you’re running a gateway or router, you need masquerading (NAT):
table inet nat { chain postrouting { type nat hook postrouting priority 100; policy accept;
oif eth0 masquerade }}This does NAT on anything leaving eth0 (your WAN interface). Everything behind your router can reach the outside world without the outside world knowing about your internal IPs.
Rate limiting (stop SSH brute force)
Want to protect SSH from brute force attacks?
chain input { type filter hook input priority 0; policy drop;
iif lo accept ct state established,related accept
# Rate limit SSH: allow 10 packets in 10 seconds tcp dport 22 limit rate 10/10s accept
tcp dport { 80, 443 } accept reject with icmp type port-unreachable}That’s it. After 10 connection attempts in 10 seconds, the rest get dropped. Your brute-force attackers move on to easier targets.
Making it stick
One-off rules are fine for testing, but you need persistence:
$ sudo nft list ruleset > /etc/nftables.conf$ sudo systemctl enable nftables$ sudo systemctl start nftablesThe systemd service reads /etc/nftables.conf on boot and loads your rules. If you ever make manual changes, dump them back to the config file so you don’t lose them.
A reality check
If you’re using UFW or firewalld, they’re sitting on top of nftables already. You can still use them—they just generate nftables rules for you. But if you want direct control (and full transparency into what’s happening), going raw nftables is worth the learning curve.
The kernel is phasing out iptables. Better to jump in now than scramble later when your distro stops shipping iptables binaries entirely.
Your 2 AM self will thank you for understanding this stuff before it becomes mandatory.