nftables is the firewall of the modern Linux kernel: it is what actually
filters packets, under UFW as under firewalld. The nft tool replaces
iptables with a unified syntax, native sets and better performance. This
guide is for those who want to write their rules directly in nftables,
without a frontend: minimalist servers, containers, appliances, or rulesets
versioned as IaC. You will start from a complete server ruleset, then
cover sets, NAT and persistence.
If you are first looking for a simple start, a frontend such as UFW or firewalld is faster. Direct nftables is for those who want full control.
What you will learn
- The difference between nftables and iptables, and when to write direct
nft. - The building blocks: tables, chains, hooks and default policy.
- A complete server ruleset with stateful filtering.
- Sets to manage address lists cleanly.
- NAT, masquerade and port forwarding.
- Making the ruleset persistent across reboots.
Prerequisites
- A recent Linux server (RHEL 8+, Debian 10+, Ubuntu 20.04+) and root access.
- The filtering concepts: default policy, inbound/outbound flows, stateful connections. If in doubt, see Introduction to firewalls.
- A fallback console access before applying a
droppolicy on a remote server.
nftables or iptables?
iptables and nftables both drive Netfilter, the kernel's
filtering subsystem. nftables is the official successor: on recent
distributions, iptables is often just a wrapper (iptables-nft) that
translates to nftables internally.
| Criterion | iptables | nftables |
|---|---|---|
| Status | Legacy, maintained for compatibility | Standard since Linux 3.13 (2014) |
| Syntax | One command per table (iptables, ip6tables...) | One nft command, IPv4 and IPv6 unified |
| IP lists | Requires ipset on top | Native sets |
| Performance | Rules evaluated linearly | Sets and maps in constant time |
| Counting | Counters by default | Counters on demand (faster) |
In short: on a modern system, write nftables. iptables is now only useful to read old configurations or to run legacy scripts.
When should you write nftables directly?
The general rule is still to use your distribution's default frontend
(UFW on Debian/Ubuntu,
firewalld on RHEL). But direct nft
is justified in several cases:
- Minimalist server or container where you want no extra daemon.
- Ruleset versioned in Git and deployed by Ansible or a script (infrastructure as code).
- Advanced rules that frontends expose poorly: maps, hook priorities, range filtering.
- Appliance or gateway where you control all the traffic.
Install nftables
nftables is present in every recent kernel; only the nft tool and the
service can be missing. The version shown below comes from a
container of each distribution.
sudo apt update && sudo apt install -y nftablessudo systemctl enable --now nftablesnft --version# nftables v1.1.3 (Commodore Bullmoose #4) <- Debian 13sudo dnf install -y nftablessudo systemctl enable --now nftablesnft --version# nftables v1.1.5 (Commodore Bullmoose #6) <- AlmaLinux 10Tables, chains and hooks
Three notions structure nftables. A table is a container of rules for a
family of protocols (inet covers IPv4 and IPv6 at once). A chain
groups rules; a so-called base chain hooks onto a kernel hook, a
point the packet passes through.
The main hooks for filtering and NAT, each matching one moment in the packet's journey through the kernel:
| Hook | Moment | Typical use |
|---|---|---|
input | Packet addressed to the machine | Filter inbound flows |
output | Packet sent by the machine | Filter outbound flows |
forward | Packet routed through the machine | Gateway, router |
prerouting | Before the routing decision | DNAT, port forwarding |
postrouting | After routing, before sending | SNAT, masquerade |
Each base chain carries a default policy (policy drop or
policy accept) applied when no rule matched, which makes it the safety
net of the whole chain.
A first server ruleset
Here is a complete server ruleset in the inet family (IPv4 + IPv6). It
sets a drop policy by default, only allows the essentials, and logs
the rest. This ruleset was validated (nft -c) then loaded on nftables 1.1.3
(Debian 13) and 1.1.5 (AlmaLinux 10).
#!/usr/sbin/nft -fflush ruleset
table inet filter { chain input { type filter hook input priority filter; policy drop;
iif "lo" accept ct state established,related accept ct state invalid drop ip protocol icmp accept meta l4proto ipv6-icmp accept tcp dport { 80, 443 } accept tcp dport 22 accept limit rate 5/minute log prefix "nft-drop: " drop } chain forward { type filter hook forward priority filter; policy drop; } chain output { type filter hook output priority filter; policy accept; }}Every line counts:
ct state established,related acceptallows replies to connections already open: it is the heart of stateful filtering. Without it, your server would receive no reply.ct state invalid dropdrops inconsistent packets, often scans.meta l4proto ipv6-icmp acceptlets ICMPv6 through, essential to IPv6: without it, neighbour discovery fails and the server's IPv6 stops working.ip protocol icmponly covers IPv4.tcp dport { 80, 443 }uses an anonymous list: more readable than one rule per port.limit rate 5/minute loglogs blocked packets without flooding the logs.
Always validate before applying, with the -c (check) option, which
loads nothing:
sudo nft -c -f /etc/nftables.conf # validation onlysudo nft -f /etc/nftables.conf # applysudo nft list ruleset # checkSets: manage address lists
A set is a named list, evaluated in constant time. It is the native
equivalent of ipset, ideal for an administration allowlist.
table inet filter { set ssh_allowed { type ipv4_addr elements = { 192.168.1.10, 10.0.0.5 } } chain input { type filter hook input priority filter; policy drop; # ... previous rules, without the "tcp dport 22 accept" line ... tcp dport 22 ip saddr @ssh_allowed accept }}The ip saddr @ssh_allowed rule only accepts SSH from the
addresses in the set. It replaces the tcp dport 22 accept line of the
first ruleset: left in place, that line would accept SSH from anywhere
before the set is ever consulted, because an accept ends the evaluation
of the chain. Adding an IP does not require reloading the whole
ruleset:
sudo nft add element inet filter ssh_allowed { 203.0.113.7 }This addition only lives in memory: also add the address to the ruleset file, otherwise it disappears at the next reboot or service reload.
Maps go further by associating a key with a value (for example a port with a decision), useful for compact rule routing.
NAT and masquerade
For a gateway to share its connection, add a nat table with a
postrouting chain. Masquerade rewrites the source address of
outgoing packets with the interface's IP, the form of SNAT suited to
dynamic IPs.
table ip nat { chain postrouting { type nat hook postrouting priority srcnat; policy accept; oifname "eth0" masquerade }}Do not forget to enable kernel routing, otherwise packets do not cross the machine:
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-forward.confsudo sysctl --systemPort forwarding with DNAT
To forward a public port to an internal service, use a prerouting chain
with dnat. Loaded without error on nftables 1.1.3:
table ip nat { chain prerouting { type nat hook prerouting priority dstnat; policy accept; tcp dport 8080 dnat to 192.168.1.20:80 }}Here, all traffic arriving on port 8080 is rewritten to
192.168.1.20:80. Combine prerouting (DNAT) and postrouting
(masquerade) for a complete gateway.
Make the ruleset persistent
A ruleset loaded with nft disappears at reboot. Two reflexes make it
permanent.
-
Write the ruleset into the file actually read by the service, starting it with
flush rulesetto start clean. This path is not the same on every distribution, and it is the first pitfall of persistence.The same
systemctl catreveals a second behaviour of the unit, on the service stop side this time, which concerns any machine where another firewall manager is already running. -
Enable the service that reloads this file at boot:
Terminal window sudo systemctl enable --now nftables -
Check after a reboot that the rules are there:
Terminal window sudo nft list ruleset
Since the file is a text script, version it in Git and deploy it with Ansible: that is the whole point of direct nftables as infrastructure as code.
nftables and Docker
Docker writes its own rules into Netfilter to publish container
ports. A manual flush ruleset erases these rules and breaks container
networking until the next restart of the Docker daemon.
The good practice: let Docker manage its chains, and put your rules in a separate table rather than emptying the global ruleset. On a gateway that also hosts containers, isolate carefully and test after each change.
nftables cheat sheet
The everyday commands to inspect and modify an active ruleset, with the handle you need for a targeted deletion and the flush to use with care.
sudo nft list ruleset # the whole rulesetsudo nft -a list ruleset # with handles (to delete)sudo nft list table inet filter # one specific tablesudo nft add rule inet filter input tcp dport 8443 acceptsudo nft delete rule inet filter input handle 12 # via the handlesudo nft flush ruleset # erase everything (careful!)Troubleshooting
The first row of the table is the one that will cost you the most: a drop
policy applied without an SSH rule cuts you off from the machine, and
only the console gets you back. Get into the habit of testing a ruleset
with a session open in parallel. The other symptoms follow a common logic: a
blocked legitimate packet almost always points to a missing ct state
rule, because without connection tracking the replies to outbound traffic are
treated as inbound traffic.
| Symptom | Probable cause | Solution |
|---|---|---|
| Locked out of the server | policy drop without an SSH rule | Console access, add the port 22 rule |
| No reply to requests | No ct state established rule | Allow established,related |
| IPv6 unreachable, IPv4 working | ICMPv6 blocked by the policy drop | Add meta l4proto ipv6-icmp accept |
| Rules lost at reboot | Ruleset written to a file the service does not read | systemctl cat nftables, write the right file, enable the service |
| Docker containers unreachable | flush ruleset erased the Docker chains | Restart docker, isolate your rules |
| Blocked packets invisible | No logging | Add a log prefix rule before drop |
Key points
- nftables is the filtering engine of every modern Linux;
iptablesis now just a wrapper. - Structure: tables (families), chains hooked onto hooks, default policy.
- A good server ruleset is stateful (
ct state established,related), drop by default, and lets ICMPv6 through. - Sets replace
ipsetfor address lists; maps go further. - NAT in
postrouting(masquerade), port forwarding inprerouting(dnat). - Make the ruleset persistent in the file read by the
nftablesservice:/etc/nftables.confon Debian/Ubuntu,/etc/sysconfig/nftables.confon RHEL.
Knowledge check
Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: SELinux: understand, troubleshoot and disable
A new full quiz with random questions
Work again only on the questions you missed
FAQ: frequently asked questions
These questions come up regularly when moving to nftables: what sets it
apart from iptables, its coexistence with UFW and firewalld, rule
persistence, NAT, and the effect on Docker container networking.
nft command for IPv4 and IPv6), native sets (no more need for ipset) and more efficient evaluation of rules. On modern distributions, iptables is often just a wrapper (iptables-nft) that translates to nftables. In practice, on a recent system, write nftables.nft directly gives full control, useful for a minimalist server, a container or a ruleset versioned as IaC. On the other hand, never mix manual nft rules with an active frontend: UFW and firewalld manage their own chains, and the two configurations contradict each other silently.flush ruleset): /etc/nftables.conf on Debian/Ubuntu, /etc/sysconfig/nftables.conf on RHEL. systemctl cat nftables shows the path actually loaded. Then enable the service that reloads this file at boot:sudo systemctl enable --now nftables
Since this file is a text script, version it in Git and deploy it with Ansible: that is the whole point of nftables as infrastructure as code.postrouting chain:table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "eth0" masquerade
}
}
Also enable kernel routing (net.ipv4.ip_forward = 1). To forward a port to an internal service (port forwarding), use a prerouting chain with dnat, for example tcp dport 8080 dnat to 192.168.1.20:80.nft flush ruleset erases these rules and cuts container networking until the next restart of the Docker daemon. On a machine that hosts containers, never empty the global ruleset: put your rules in a separate table, let Docker manage its chains, and test after each change.