LinuxXXV · Firewallsnftables
nftables architecture - tables, chains, and rules
What you'll learn
- Describe nftables tables, chains, hooks, and rules
- Write a basic ruleset
- Combine IPv4 and IPv6 rules in inet tables
- Persist the ruleset across reboots
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09
nftables replaces iptables as the modern Linux firewall. It uses a cleaner syntax, combines IPv4 and IPv6 in one table, and is faster. This lesson covers the structure.
The hierarchy
Family (ip, ip6, inet, bridge, netdev, arp)
Table (filter, nat, mangle, raw, security)
Chain (a sequence of rules with a hook and a priority)
Rule (a match expression and a verdict)
- Family:
inetcombines IPv4 and IPv6 - usually what you want. - Table:
filteris the standard for accept/drop decisions. - Chain: a list of rules. Built-in chains attach to netfilter hooks.
- Rule: matches packets and applies a verdict (accept, drop, reject, queue, return).
The basic commands
nft list ruleset # show everything
nft list tables # show tables only
nft list table inet filter # show one table
nft list chain inet filter input # show one chain
nft flush ruleset # delete everything
nft delete table inet filter # delete one table
A minimal ruleset
nft add table inet filter
nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }
nft add chain inet filter forward { type filter hook forward priority 0 \; policy drop \; }
nft add chain inet filter output { type filter hook output priority 0 \; policy accept \; }
# Allow established/related
nft add rule inet filter input ct state established,related accept
# Allow loopback
nft add rule inet filter input iif lo accept
# Allow SSH from management network
nft add rule inet filter input ip saddr 10.0.0.0/24 tcp dport 22 accept
# Allow ICMP from monitoring
nft add rule inet filter input ip saddr 10.0.0.0/24 ip protocol icmp accept
# Drop invalid
nft add rule inet filter input ct state invalid drop
# Log what is about to hit the chain's drop policy.
# `add` appends, so this rule runs last - after every accept.
# `limit rate` is mandatory: an unlimited log rule is a remote
# disk-fill vector.
nft add rule inet filter input limit rate 5/minute burst 10 packets \
log prefix "nft-drop: " level warn
The default policy is drop on INPUT and FORWARD; OUTPUT is
accept because the host must make outbound connections.
Rule syntax
nft add rule <table> <chain> <matches> <verdict>
Common matches:
| Match | Meaning |
|---|---|
iifname "<iface>" | Incoming interface by name, matched per packet — use this |
oifname "<iface>" | Outgoing interface by name, matched per packet — use this |
iif <iface> | Incoming interface by index, resolved once at load time |
oif <iface> | Outgoing interface by index, resolved once at load time |
ip saddr <addr> | Source IP (or CIDR) |
ip daddr <addr> | Destination IP |
ip protocol <proto> | IP protocol (tcp, udp, icmp) |
tcp dport <port> | TCP destination port |
udp dport <port> | UDP destination port |
ct state <state> | Connection tracking state |
tcp flags <flags> | TCP flags |
Statements come in two kinds, and the difference decides
whether the rest of the chain still runs. Confusing them is
how a ruleset ends up with rules below a counter that the
author believed were unreachable, or above an accept that
the author believed still got evaluated.
Verdicts (terminating — evaluation of the chain stops):
| Verdict | Meaning |
|---|---|
accept | Allow the packet, leave the chain |
drop | Silently discard |
reject | Discard and send ICMP unreachable (or TCP RST) |
queue | Hand the packet to a userspace program |
return | Leave this chain, resume in the caller |
jump / goto | Transfer to another chain |
Non-terminating statements (the packet carries on to the next rule):
| Statement | Meaning |
|---|---|
counter | Count packets and bytes, decide nothing |
log prefix "..." level warn | Log and continue |
limit rate ... | Match only within a rate, usually paired with a verdict |
meta mark set 0x1 | Set the packet mark, used by policy routing |
ct helper set "ftp-standard" | Assign a conntrack helper |
NAT statements — snat, dnat, masquerade, redirect —
are a third category. They are valid nftables, not iptables
leftovers, and they belong in a nat-type chain. redirect
is a special form of dnat that always translates to the
local host’s address, so it is only valid in prerouting and
output.
Conntrack (stateful) matches
nft add rule inet filter input ct state established,related accept
nft add rule inet filter input ct state invalid drop
nft add rule inet filter input ct state new tcp dport 22 accept
States: new, established, related, invalid. Most rules
allow established,related first (so response traffic flows),
then block invalid, then allow specific new flows.
Sets for many addresses
If you have many addresses to allow (e.g. an IP allowlist):
nft add set inet filter allowed_nets { type ipv4_addr \; flags interval \; }
nft add element inet filter allowed_nets { 10.0.0.0/24, 192.168.1.0/24 }
nft add rule inet filter input ip saddr @allowed_nets accept
Sets are efficient (interval matching) and make rules maintainable.
Persist across reboots
nftables rules are kernel state. They do not survive a
reboot. Dump the live ruleset into /etc/nftables.conf, but
prepend flush ruleset so the file is self-contained:
# Write a loadable, idempotent ruleset file
{ printf '#!/usr/sbin/nft -f\n\nflush ruleset\n\n'; sudo nft list ruleset; } \
| sudo tee /etc/nftables.conf
sudo chmod 0750 /etc/nftables.conf
sudo nft -c -f /etc/nftables.conf # verify before enabling
# On boot, systemd runs nftables.service which loads the file
sudo systemctl enable nftables
sudo systemctl start nftables
The default /etc/nftables.conf on most distros has a
placeholder ruleset; replace it with your own.
Atomicity
nftables can apply a ruleset atomically using a file:
sudo nft -c -f /etc/nftables.conf # check syntax, change nothing
sudo nft -f /etc/nftables.conf # atomic, but ADDITIVE - it only
# replaces the ruleset if the file
# starts with 'flush ruleset'
Atomic and replacing are two different guarantees. Atomic
means the whole file is committed in one transaction: no
window where half the rules are live. It does not mean the
previous ruleset is discarded. Discarding is what
flush ruleset at the top of the file does, and it is
inside the same transaction, so the flush-and-load pair is
still atomic - the host is never left unfirewalled.
For remote changes, write the file first, validate syntax, then load.
Knowledge check
Knowledge check · 5 questions
Q1. Which nftables family combines IPv4 and IPv6 rules?
Q2. nftables rules survive a reboot.
Q3. Which of the following are terminating nftables verdicts - statements that stop evaluation of the chain? Select all that apply.
Q4. A host persists its firewall with `nft list ruleset > /etc/nftables.conf`. After three change-and-reload cycles the input chain has 18 rules where the operator expects 6, and deleting an allow rule does not close the port. What happened?
Q5. Why must `nft insert rule ... log prefix "nft-drop: "` not be used to log dropped packets?
Passing score: 75%. Answers are checked in this browser.