SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor

iptables vs nftables for Ubuntu: Which One You Dey Run?

Ubuntu iptables fit dey write nftables rules. Run exact checks, read the native ruleset, and see why ufw and Docker fit collide on your server.

iptables vs nftables for Ubuntu: which one your box dey run?

For Ubuntu 20.04 and later, the iptables command na front end wey dey write nftables rules. Na one packet filter dey run for kernel, nftables, while two user space commands dey program am. An iptables -A INPUT line still dey work exactly as e dey work before, and the rule wey e create na nftables rule wey nft fit print.

Confirm am for your own server before you trust am.

iptables -V
sudo update-alternatives --display iptables
sudo nft list ruleset

For Ubuntu 24.04 (iptables 1.8.10, as of August 2026), iptables -V dey print iptables v1.8.10 (nf_tables). The name for brackets na the back end. (nf_tables) mean say the command dey talk to nftables. (legacy) mean the old x_tables back end, wey Ubuntu still dey ship as iptables-legacy and wey kernel dey keep as completely separate ruleset. update-alternatives dey print the symlink behind that choice: link currently points to /usr/sbin/iptables-nft.

For fresh VPS wey no firewall configure, sudo nft list ruleset no dey print anything. That empty output na your baseline. Add one rule the old way, then check am again.

sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo nft list ruleset
# Warning: table ip filter is managed by iptables-nft, do not touch!
table ip filter {
  chain INPUT {
    type filter hook input priority filter; policy accept;
    tcp dport 22 counter packets 0 bytes 0 accept
  }
  chain FORWARD {
    type filter hook forward priority filter; policy accept;
  }
  chain OUTPUT {
    type filter hook output priority filter; policy accept;
  }
}

Your iptables rule na nftables rule. iptables-nft dey mark the tables wey e create, and nft dey print that warning when e see the mark, because editing that kind table with nft go make two tools dey control the same rules. Look wetin one command produce: table wey you no name, and chains wey you no request. Na the old model be that, and na the first thing wey change when you write nftables directly.

Wetin iptables -L dey hide from you

iptables -L dey show only the filter table. NAT (network address translation) rules need iptables -t nat -L, and mangle rules need -t mangle. IPv6 dey use separate command, ip6tables, with its own copy of every rule. So, one box fit look clean for one listing, while something dey drop or rewrite your packets from table wey you never check.

sudo nft list ruleset dey print every family, every table, every chain and every rule for one output. For server wey you no build yourself, na this one command dey give you the fastest way to see wetin really dey loaded. Add -a to print rule handles, wey you need if you wan delete one rule instead of the whole chain.

Two habits dey worth correcting while you dey here. iptables -L dey resolve addresses and ports into names, so for box wey resolver spoil, e fit look like say command hang: use iptables -nvL. And confirm say the legacy back end empty with sudo iptables-legacy -nvL, because if rules dey for both back ends, kernel go evaluate both, and neither listing go show you the complete picture.

Tables and chains wey you create, no be ones wey you inherit

nftables start with nothing. No filter table dey until you create one, and the word filter na only name wey you choose. A chain go see packets only when you give am a type, a hook and a priority, and that one make am a base chain. Chain wey no get those things fit only receive traffic through explicit jump or goto, so e no cost anything until something jump go am.

The other major change na the inet family. One inet table fit handle IPv4 and IPv6 with the same rules. This remove one whole class of bug where port dey closed for iptables but e wide open for ip6tables. This mismatch common enough to get im own failure mode for ufw boxes.

Here na complete server ruleset. Put am for /etc/nftables.conf.

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
  set admin_ips {
    type ipv4_addr
    flags interval
    elements = { 203.0.113.5, 198.51.100.0/24 }
  }

  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    ct state invalid drop
    iif lo accept
    ip protocol icmp accept
    meta l4proto ipv6-icmp accept
    tcp dport 22 ip saddr @admin_ips counter accept
    tcp dport { 80, 443 } counter accept
  }

  chain forward {
    type filter hook forward priority filter; policy drop;
  }

  chain output {
    type filter hook output priority filter; policy accept;
  }
}

Read line 2 twice. flush ruleset go delete every table for the box, including tables wey ufw and Docker create for themselves. Continue reading before you run this for live server.

The first rule for the input chain dey do most of the work. ct state established,related accept allow replies to connections wey you start to come back in, so the rest of the chain only need decide about new connections. ct state invalid drop throw away packets wey no match any known connection or valid connection start. Everything after that na explicit hole, and policy drop handle the rest.

Check the file before you load am, and keep second SSH session open while you do am. policy drop plus one typo for the SSH rule fit lock you out of your own server.

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

nft -c -f parse the file and report errors without loading anything. Clean parse no print any output at all.

Set fit replace long rule lists

tcp dport { 80, 443 } na anonymous set: one rule and one lookup, instead of one rule for each port. Named set like admin_ips go further, because you fit change am while firewall still dey run.

sudo nft add element inet filter admin_ips { 203.0.113.9 }
sudo nft list set inet filter admin_ips
sudo nft delete element inet filter admin_ips { 203.0.113.9 }

No reload, no rule renumbering, and the match remain one lookup whether the set get five addresses or fifty thousand. flags interval na wetin allow set hold ranges and CIDR (classless inter-domain routing) prefixes like 198.51.100.0/24. Without that flag, the set go accept single addresses only, and loading the prefix go fail.

Sets fit also make their own elements expire.

set banned {
  type ipv4_addr
  flags timeout
  timeout 1h
}

With rule ip saddr @banned drop, each element go remove itself one hour after dem add am. Na so the nftables action for fail2ban for Ubuntu 24.04 dey ban address: e add element to set; e no add rule. If ports still new matter for you, start with wetin port really be for Linux.

One difference dey catch people during migration. nftables no dey count packets unless you ask am to. iptables -nvL dey show counters for every rule, always. For nftables, na only rules wey carry counter keyword get numbers, so put counter for any rule wey you expect to debug later.

How hooks and priorities decide the order

A base chain dey name one hook. Na the point for packet path wey e go run. prerouting dey run before routing decision. input dey run for packets wey target this machine. forward dey run for packets wey dem route through am. output dey run for packets wey local processes create. postrouting dey run last, just before packet comot.

Priority dey arrange chains inside one hook. The number wey low go first. nftables get names for the classic values: raw na -300, mangle na -150, dstnat na -100, filter na 0, srcnat na 100. Writing priority filter; mean the same thing as writing priority 0;.

Now na this part decide whether mixing tools go work. Every base chain wey register for one hook go run, according to priority. If packet get accepted for your chain, e no mean say processing don finish: accept stop only that chain, and packet continue go the next base chain for the same hook. drop final everywhere and e stop packet immediately. So, permissive rule for your table no fit cancel drop for ufw table, no matter which one run first. Your accept no go protect you from chain wey run later.

Two base chains for the same hook wey get the same priority go run according to registration order. The service wey start first dey determine this order. The order fit change after reboot. If you must run your own table alongside ufw, give am a different priority. This one write down the order instead of making the services race for am.

Why reverse NAT rule no dey to write?

Na this question people dey get wrong pass, so make we give direct answer. Connection tracking dey write the reverse translation for you. No second rule dey to add.

A nat table wey dey do both sides of the normal VPS work fit look like this.

table inet nat {
  chain prerouting {
    type nat hook prerouting priority dstnat; policy accept;
    iifname "enp1s0" tcp dport 8080 dnat ip to 10.0.0.5:80
  }

  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    ip saddr 10.0.0.0/24 oifname "enp1s0" masquerade
  }
}

Na only the first packet for a connection nat chain dey check. When rule match, kernel dey store that translation for connection tracking table together with the connection entry. Every later packet, for both directions, dey rewrite from the stored entry, and no rule dey read again. Install the conntrack tool and look one live entry.

sudo apt install -y conntrack
sudo conntrack -L -p tcp
tcp 6 431999 ESTABLISHED src=198.51.100.20 dst=203.0.113.10 sport=54321 dport=8080 src=10.0.0.5 dst=198.51.100.20 sport=80 dport=54321 [ASSURED] mark=0 use=1

Read am as two tuples. The first four fields na the connection as client send am, addressed to 203.0.113.10:8080, your public address. The second four na the reply wey kernel dey expect, already reversed and already translated, coming from 10.0.0.5:80, the real backend. That second tuple na the reverse rule. Kernel write am when the first packet match.

So no write rule for the return direction. E no fit match, because return packets belong to established connection and dem never reach nat chain. And if e somehow match, you go translate packet wey kernel don already fix.

Where rewrite suppose dey follow from the same mechanism. Destination translation must run for prerouting, before routing decision, because routing must see the new destination or the packet go wrong place. Traffic wey the box generate by itself dey handle for output hook for the same reason. Source translation, including source port rewrite, must run for postrouting, after routing don choose the outgoing interface. masquerade dey take im address from that interface, and interface no dey known until routing don run.

Na why rule like this belong for the end of the path and nowhere else.

ip saddr 10.0.0.0/24 oifname "enp1s0" snat ip to 203.0.113.10:20000-30000

The port range dey rewrite source port together with source address. Na wetin you want when many internal clients share one public address and their source ports collide. Reply go arrive addressed to port inside that range, conntrack go match am to the entry, and original source port go return before packet deliver. Again, no second rule.

One practical result be say: changing NAT rule no dey move connections wey already exist, because their translation don already store. Dem go continue with the old behaviour until their entries expire. sudo conntrack -D -p tcp --dport 8080 dey delete matching entries and sudo conntrack -F dey delete all of dem. Handle the second one carefully for NAT box, because na those stored translations dey keep current connections alive. Flushing dem go break every connection through the box at once.

ufw and Docker dey write dia own rules

ufw na front end for iptables, wey for Ubuntu na front end for nftables. So, ufw box get one ip filter table wey full of chains wey dem name ufw-before-input, ufw-user-input and so on, plus one ip6 filter copy of the same structure. Use sudo nft list ruleset | grep ufw check am. Files for /etc/ufw dey generate those chains, and ufw reload dey rewrite dem from scratch. Na why hand-written iptables rule wey you add on top go disappear for the next reload. The ufw basics for a VPS explain how that file layout dey work.

Docker dey configure firewall by itself, and e no consult ufw. When you publish port with -p 80:80, e dey write DNAT rule inside nat table and accept rule for the forward path. Both of dem run before ufw user chains. The result fit surprise person: ufw deny 80 don load, but container still dey reachable from internet. The fix dey inside DOCKER-USER chain wey Docker leave for your rules, and why Docker containers ignore ufw explain how e work. Use sudo nft list ruleset | grep -i docker see wetin dey for your box.

Now read that flush ruleset line from the config above again. E dey delete every table, including the tables wey those two tools manage. For Docker host, published ports go stop working until sudo systemctl restart docker rebuilds the chains. That one line na the commonest way people take their own services offline while dem dey arrange firewall.

Rules wey go survive reboot

No ruleset dey persistent by itself. Kernel dey forget everything when system shut down, and each side dey solve am with separate package.

For nftables, /etc/nftables.conf na nftables.service wey dey read am. Ubuntu dey ship that service disabled, so check am before you trust am.

systemctl is-enabled nftables
sudo nft -c -f /etc/nftables.conf
sudo systemctl enable --now nftables

For iptables, package na iptables-persistent. E install netfilter-persistent and save to /etc/iptables/rules.v4 and /etc/iptables/rules.v6.

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

No run both. Two files wey each claim say dem hold firewall go dey different over time, and the one wey load last go win for way wey nobody fit predict by reading either file.

Another related trap dey when you dump live ruleset. sudo nft -s list ruleset > /etc/nftables.conf dey capture everything wey load for that moment, including ufw tables and Docker tables. If you restore that for boot, you go get frozen copy of rules wey those tools expect to build by themselves, then second copy once dem start. Dump only your own table with sudo nft -s list table inet filter. -s flag dey leave out counters, wey no belong inside config file.

Make you switch on native firewall for your VPS?

Leave ufw as e dey unless you need something wey e no fit express. ufw dey handle the normal VPS work: default deny with small number of open ports. If you replace am with ruleset wey you write by hand just for the sake of am, you go still get the same firewall plus one extra thing to maintain.

Use native firewall when wetin you need dey outside ufw model: NAT and port forwarding, sets wey you dey update at runtime, one rule wey cover both address families, or chain priorities wey you choose yourself. Those ones na real reasons, and ufw no get way to provide dem.

If you choose native firewall, use am completely. Run sudo ufw disable and sudo systemctl disable --now ufw, confirm with sudo nft list ruleset say its tables don clear, then load your own file. If ufw and hand-written table dey run together for one box, traffic still go pass, but the live policy now na combination of 2 rulesets. Service startup go decide the order wey dem dey evaluate. Nobody wey read either file fit tell you exactly wetin the box dey do.

IpTables ruleset wey already dey there

iptables-translate go convert one rule and print the nftables version. E no go change anything for the server.

iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
nft add rule ip filter INPUT tcp dport 22 counter accept

iptables-restore-translate -f /etc/iptables/rules.v4 go do the same thing for one complete saved ruleset. Treat wetin e print as first draft. The conversion na mechanical, rule by rule. So e go bring back the old table and chain names, two separate rulesets for IPv4 and IPv6, and none of the sets wey make the migration useful. Rewrite am by hand as one inet table, then check am with nft -c -f before you use am for live server.

The addresses for these examples come from the documentation ranges 203.0.113.0/24 and 198.51.100.0/24, while enp1s0 na interface name. Get your own values from ip route show default and ip -br addr instead of copying mine, because current Ubuntu images rarely name any interface eth0.

FAQ

iptables don deprecated for Ubuntu?

The command no dey comot, and e still dey work for Ubuntu 24.04. Wetin change na wetin dey happen underneath: iptables na front end wey dey write nftables rules through iptables-nft back end. Check your own with iptables -V; e go print iptables v1.8.10 (nf_tables) for 24.04. The old x_tables back end still dey ship as iptables-legacy, and e get completely separate ruleset, so put rules for one back end, no put am for both.

I need second rule to undo NAT on the way back?

No. Connection tracking dey store the translation when the first packet for a connection match nat rule, and every later packet for both directions dey rewrite from that stored entry. sudo conntrack -L dey show am as two tuples for each connection: the original direction, then the reply wey don already reverse. Rule wey you write for return direction no fit help, because return packets no dey reach nat chain.

I fit run ufw and my own nftables rules at the same time?

E go work, but you dey create problem for yourself. Every base chain for a hook dey run, so the live policy na both rulesets join together, ordered by priority and, when priority equal, by whichever service start first. A drop for either one na final, and an accept for your own rules no dey stop the other one from dropping the same packet. Pick one tool. If na nftables, disable ufw first and confirm say its tables don comot from sudo nft list ruleset.

How I go make nftables rules survive reboot for Ubuntu?

Put the ruleset for /etc/nftables.conf, check am with sudo nft -c -f /etc/nftables.conf, then run sudo systemctl enable --now nftables. The service no dey enabled by default, so systemctl is-enabled nftables worth running once. When you generate that file, dump only your own table with sudo nft -s list table inet filter, because full list ruleset dump also dey capture the tables wey ufw and Docker dey manage by themselves.