Ramblings of an aging IT geek
← Ramblings of an aging IT geek
linux

i finally moved off iptables, and it was overdue

Migrating a handful of long-lived hosts from iptables to native nftables rules, and why the result is easier to read than what it replaced.

A terminal showing a Linux firewall ruleset

I have been running the same iptables ruleset, with minor variations, since roughly 2014. It works. It has always worked. And every time I opened it I felt that small pang of "I do not actually want to touch this", which is exactly the feeling you should distrust in infrastructure. So this week I finally moved a few hosts to native nftables, and the only thing I regret is not doing it sooner.

The trigger was boring: I was adding one rule. One. A new service, one port, source-restricted to the management subnet. With my old iptables-restore file that meant finding the right chain, getting the rule ordering right relative to the established/related accept, remembering whether I was on the box with the legacy script or the one I'd half-converted, and then crossing my fingers. The cognitive load for one port was absurd.

what actually changed

nftables collapses the four-table-ish mess of iptables, ip6tables, and friends into one framework with one tool. You get tables, chains, and sets, and you write IPv4 and IPv6 in the same place. That last bit alone is worth the move. For years I maintained two parallel rulesets that were supposed to be identical and, of course, occasionally weren't.

Here is the shape of the inbound chain I ended up with, which I find genuinely readable:

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        ct state established,related accept
        ct state invalid drop
        iif "lo" accept

        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        tcp dport 22 ip saddr 10.0.0.0/24 accept
        tcp dport { 80, 443 } accept
    }
}

table inet means it covers both address families at once. The set syntax { 80, 443 } is just nicer than two near-identical lines, and it compiles down to an actual kernel set rather than a sequence of linear matches, so it scales sensibly when the list gets long.

the migration, in practice

I did not use the iptables-translate helper as the final answer, though it is useful for sanity-checking. Auto-translated rules read like auto-translated rules: technically correct, full of xt compatibility cruft, and no easier to maintain than the originals. The whole point was to end up with something I'd be happy to read in two years, so I rewrote by hand. It took an evening per host, mostly spent deleting accumulated rules nobody could explain.

The one genuine gotcha: on Debian the package is nftables, the service that loads /etc/nftables.conf at boot is nftables.service, and you want to make sure the old iptables rules are not also being restored by some other unit. I had a host where both were loading, the nft policy was drop, and the leftover iptables ACCEPT rules in the legacy tables made the behaviour confusing for a good twenty minutes. Pick one. Disable the other. nft list ruleset shows you the truth.

I am not going to pretend this was a heroic project. It was an afternoon of tidying that I'd been deferring for the better part of a decade. But the ruleset is now half the length, covers v4 and v6 in one file, and adding that one port took thirty seconds. Sometimes the overdue job is the satisfying one precisely because it was overdue.

A rack-mounted server running the new firewall

If you are still on legacy iptables on hosts you actually care about, this is your gentle nudge. The tooling is stable, it has been the default on most distributions for a while now, and the day you need to change one rule in a hurry you will be glad it reads like something a human wrote.