Cheat Sheet

nftables Cheat Sheet

nftables is the firewall that replaced iptables in the Linux kernel, driven by a single nft command. This sheet covers listing the ruleset, adding and deleting rules by handle, the match syntax, sets, NAT, rate limits, logging, keeping rules across a reboot, and translating the iptables rules you already have.

Last updated September 11, 2026

Listing Rules

Everything here needs root. prints nothing at all when a table does not exist, which usually means the firewall is empty rather than broken.

CommandWhat it does
Every table, chain, and rule on the box
The same, with the you need in order to delete a rule
Just the table families and names
One table with its chains and rules
One chain
Only the IPv4 family (, , , , also work)
Stateless output: no counters, ready to feed back in with
JSON, for scripts
Named sets and their elements
Named counters
Find the handle of the rule that mentions a port
Whether the service that loads rules at boot is running
nft version; named priorities need 0.9 or newer

From iptables

Most people land here with an iptables ruleset already running. prints the nft equivalent of any iptables command without touching the firewall.

CommandWhat it does
Print the nft version of one rule, change nothing
Same for IPv6
Dump the current iptables ruleset
Convert the whole dump to nft syntax
Load the converted file (read it first, the output is rarely perfect)
means your iptables commands already write nftables rules
See the rules that iptables-nft wrote underneath

Paste a ruleset into the iptables to nftables converter for the same job in a browser, or the firewall rule generator to write one rule for nft, iptables, UFW, and firewalld at once.

iptablesnftables
Re-declare the chain with
/ /
Native inside the ruleset
Separate and rulesOne rule in the family

The full iptables side of the mapping is on the iptables cheat sheet.

Tables and Chains

nftables ships with nothing: no filter table, no INPUT chain. You create what you need. The family covers IPv4 and IPv6 in one place and is the right default.

CommandWhat it does
Create a table (the name is yours; is convention)
A table in the IPv4 family, for NAT
Delete a table and every chain and rule in it
Empty a table but keep its chains
A plain chain, reached only by or
Delete a chain (it must be empty and unreferenced)
Rename a chain

A chain only sees traffic if it is a base chain, meaning it declares a type, a hook, and a priority. The shell eats , , and , so quote the block.

sudo nft add table inet filter
sudo nft add chain inet filter input '{ type filter hook input priority 0; policy accept; }'
sudo nft add chain inet filter forward '{ type filter hook forward priority 0; policy drop; }'
sudo nft add chain inet filter output '{ type filter hook output priority 0; policy accept; }'
HookWhere it sits
Before the routing decision; NAT destination rewrites go here
Packets addressed to this machine
Packets routed through this machine
Packets this machine generates
After routing, on the way out; NAT source rewrites go here

Chain types are , , and . Priority is a number, low first, and named values read better: (-300), (-150), (-100), (0), (100). Two base chains on the same hook both run, in priority order.

SSH Lockout

A base input chain with and no rules kills the session you are typing in, not just the next one, because nftables judges every packet including the ones carrying your keystrokes. Build the allow rules first and set the policy last.

sudo nft add table inet filter
sudo nft add chain inet filter input '{ type filter hook input priority 0; policy accept; }'
sudo nft add rule inet filter input ct state established,related accept
sudo nft add rule inet filter input ct state invalid drop
sudo nft add rule inet filter input iif lo accept
sudo nft add rule inet filter input tcp dport 22 accept
# only now, once port 22 is open:
sudo nft add chain inet filter input '{ type filter hook input priority 0; policy drop; }'
CommandWhat it does
Deadman switch: wipes the firewall in 5 minutes if you lose the session
What to run from the console if you are already locked out
Check the accept rule is live before you change the policy
from a second terminalTest with a new connection, keep the old one open

Cancel the deadman switch once the new ruleset works: , or list the pending jobs with . If SSH listens somewhere other than 22, open that port instead, and check with the SSH port checker from outside the box. The SSH cheat sheet covers the client side of a connection that will not come back.

Adding Rules

appends to the end of a chain, puts the rule at the top. Order matters: the first rule with a verdict wins, so a above an is the end of the story.

CommandWhat it does
Append a rule
Put a rule first
Insert before the rule with handle 9
Insert after the rule with handle 9
Position by index, counting from 0
Swap one rule for another, keeping its place
Several ports in one rule (quote the braces in a shell)
Block one address
Everything except an internal network
Attach a comment that survives in

Rule Syntax

A rule is a list of matches followed by a verdict. Everything before the verdict has to be true for the verdict to apply.

MatchWhat it matches
Source IPv4 address or network
Destination IPv4 address
Source IPv6 address
Destination TCP port
Source port range
Several UDP ports
Port regardless of TCP or UDP
Traffic belonging to a connection you already allowed
The first packet of a connection
Packets conntrack cannot place; drop these early
Incoming interface by name ( for outgoing)
Interface by index: faster, but breaks if the device is recreated
Wildcard interface name
Layer 4 protocol
Ping ( for IPv6)
TCP flag match
Source MAC address
Packet mark set elsewhere
The local user that owns the socket (output chain only)
VerdictWhat it does
Let the packet through, stop evaluating this chain
Discard silently; the sender waits for a timeout
Discard and answer ()
Answer a TCP connection with RST, so it fails instantly
Run another chain and come back if it returns no verdict
Run another chain and never come back
Leave this chain for the one that jumped here
No verdict: count the packet and keep evaluating
Hand the packet to a userspace program

Variables keep a file readable. at the top, then in the rules.

Deleting and Flushing

There are no rule numbers. Deletion works by handle, and handles only appear with , which is where this stops being obvious.

CommandWhat it does
Print every rule with its
Handles for one chain only
Delete one rule
Empty a chain, keep the chain and its policy
Empty every chain in a table
Delete an empty, unreferenced chain
Delete a table and everything inside it
Wipe every table: the firewall is then completely open
Reload from the file, discarding live edits (the file starts with )

Handles are assigned in order but are not reused and do not survive a reload, so list them again after every .

Sets and Maps

Sets replace ipset. A named set is part of the ruleset, so it saves and reloads with everything else, and lookups stay fast as it grows.

sudo nft add set inet filter blocklist '{ type ipv4_addr; flags interval; }'
sudo nft add element inet filter blocklist '{ 203.0.113.0/24, 198.51.100.7 }'
sudo nft add rule inet filter input ip saddr @blocklist drop
CommandWhat it does
Every named set with its elements
One set
Add an address
Remove an address
Empty a set, leave the rules pointing at it
A set of ports instead of addresses
Allows CIDR ranges as elements
Elements expire on their own, fail2ban style
A timeout on one element
Verdict map: one rule, one lookup, many ports

A map pairs a key with a value, which is how you write a port forwarding table in one rule:

sudo nft add map ip nat porthost '{ type inet_service : ipv4_addr; }'
sudo nft add element ip nat porthost '{ 80 : 10.0.0.5, 8443 : 10.0.0.6 }'
sudo nft add rule ip nat prerouting dnat to tcp dport map @porthost

NAT and Port Forwarding

NAT needs its own table with type chains; a filter chain cannot rewrite addresses. Forwarding between interfaces also needs and a forward chain that accepts the traffic.

sudo nft add table ip nat
sudo nft add chain ip nat prerouting '{ type nat hook prerouting priority dstnat; }'
sudo nft add chain ip nat postrouting '{ type nat hook postrouting priority srcnat; }'
sudo nft add rule ip nat postrouting oifname "eth0" masquerade
sudo nft add rule ip nat prerouting iifname "eth0" tcp dport 8080 dnat to 10.0.0.5:80
StatementWhat it does
Source NAT using the outgoing interface's current address (dynamic IPs)
Source NAT to a fixed address
Send matching traffic to an internal host and port
Same host, same port
Send traffic to another port on this machine
Turn on routing (add it to to keep it)
Let the forwarded, rewritten traffic through the filter chain

A dnat rule that does nothing is usually a forward chain with and no matching accept, or left at 0. Traffic from the same machine does not pass through , so a local test of a port forward always fails; test from another host, or with the SSH port checker.

Rate Limiting

matches while traffic is under the rate; matches once it goes above. Put the verdict that suits the one you picked.

CommandWhat it does
Accept up to 3 new SSH connections a minute, rest falls through
Drop new SSH connections above 3 a minute
Allow a short burst before the limit bites
Limit by bandwidth rather than packet count
Answer ping, but not a flood of it

A plain counts every packet together, so one noisy address starves everyone. For a per-address limit, use a dynamic set:

sudo nft add set inet filter ssh_meter '{ type ipv4_addr; flags dynamic; timeout 1m; }'
sudo nft add rule inet filter input tcp dport 22 ct state new \
  add @ssh_meter { ip saddr limit rate 3/minute } accept

Each source address gets its own counter. Over the limit the statement stops matching, so the packet falls through to whatever comes next, which should be a drop rule. For a single server, does the same thing in one line; see the UFW cheat sheet.

Counters and Logging

Unlike iptables, counters are off unless you ask for them, which is why an nftables ruleset is cheaper to evaluate.

CommandWhat it does
Count packets and bytes on one rule
Counters appear inline on the rules that have them
Zero every counter
Zero the counters in one table
A named counter several rules can share
Point a rule at the named counter
Log and count before dropping
Syslog level for the line ( is the default)
Include the full header details
Keep a busy drop rule from flooding the log
Watch the drops live
Send to ulogd2 over nflog instead of the kernel log

Logging happens where the rule sits, so a log rule at the bottom of a chain shows you exactly what the firewall is turning away. journalctl has the rest of the reading options.

nftables.conf

One file holds the whole ruleset. on the first line makes loading it idempotent: the file replaces what is running instead of adding to it.

#!/usr/sbin/nft -f

flush ruleset

define wan = "eth0"
define admin = { 203.0.113.10, 198.51.100.0/24 }

table inet filter {
	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
		ip6 nexthdr icmpv6 accept

		ip saddr $admin tcp dport 22 accept
		tcp dport { 80, 443 } accept

		limit rate 5/minute log prefix "nft-input-drop " counter
	}

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

	chain output {
		type filter hook output priority filter; policy accept;
	}
}
Path or lineWhat it is
The file loads on Debian, Ubuntu, and Arch
The same on RHEL, Rocky, and Fedora
Split a large ruleset across files
Lets you run the file directly if it is executable
A variable, used later as
Must be first, or a reload stacks a second copy of every rule

Persist and Reload

Rules added with are live immediately and gone at the next reboot. Nothing writes them to disk for you.

CommandWhat it does
Save the running ruleset (run it under , the redirect needs root too)
Load the file at every boot, starting now
Check it will actually run at boot
Reload the file into the kernel, atomically
The same, through the service
Unload the ruleset, leaving the box open
Install on Debian and Ubuntu ( on RHEL)

Saving and enabling, in full:

sudo sh -c 'echo "flush ruleset" > /etc/nftables.conf && nft -s list ruleset >> /etc/nftables.conf'
sudo nft -c -f /etc/nftables.conf
sudo systemctl enable --now nftables

is atomic: the whole file applies or none of it does. A reload never leaves the firewall half configured, which is the main reason to edit the file rather than type rules one at a time.

Syntax Check

parses and checks without applying, so a broken file never reaches the kernel. Run it before every reload.

CommandWhat it does
Check the whole file, change nothing
Check one command before running it
The long form of the same thing
Read a ruleset from standard input (useful in scripts)
Show what would be sent to the kernel

An error names the file, line, and column, with a caret under the token it choked on:

/etc/nftables.conf:14:17-20: Error: syntax error, unexpected string
		tcp dport 80 ACCEPT
		             ^^^^^^

The usual causes: iptables words in an nft file (, , uppercase verdicts), unquoted , , or on a shell command line, a base chain missing its line, a set used before it is declared, and reading a file that was written for .

nftables vs iptables

Topiciptablesnftables
Commands, , , One , with an family covering v4 and v6
Starting pointBuilt-in filter, nat, and mangle tablesEmpty: you create the tables and chains
DeletingBy line numberBy handle from
Big address lists, a separate tool and databaseNative sets and maps in the ruleset
Reloads, atomic per table, atomic for the whole file
CountersOn every rule, alwaysOnly where you write
Matching many portsOne rule each, or the multiport module or a verdict map lookup
StatusMaintained, but the command is usually a shimThe kernel's firewall since 3.13, default on Debian 10, Ubuntu 20.04, RHEL 8 and newer

Existing iptables scripts keep working through , so there is no rush. New rulesets are worth writing in native nft syntax.

Gotchas

  • A base chain with and no rules drops the packets carrying your SSH session, immediately. Add and the port 22 rule before you set the policy.
  • deletes the tables, so the policies go with them and the box ends up wide open. That is the opposite of the iptables trap, where flushing with a DROP policy locks you out. Both are worth a deadman switch.
  • The shell, not nft, is what rejects . Wrap the braces in single quotes.
  • Handles are not line numbers. They do not survive a reload, so re-run after every before deleting anything.
  • A rule added with is not in , and a rule written in that file is not live. Nothing syncs them.
  • / match the interface index and go stale when a device is recreated, which happens with VPN and container interfaces. Use / unless you have measured that you need the speed.
  • Docker writes its own rules and publishes container ports through the forward path, so a container with is reachable even when your input chain says otherwise. Bind it to and put a reverse proxy in front.
  • Two base chains on the same hook both run. If a rule seems to be ignored, look for another table hooking the same place at a lower priority, including the tables and firewalld create.
  • shows nothing when there are no tables. That is an open firewall, not a broken command.

nftables FAQ

How do I delete a single nftables rule?

By handle, not by line number. Run sudo nft -a list ruleset to print every rule with a trailing # handle N comment, find the rule you want, then sudo nft delete rule inet filter input handle 7. Without -a there are no handles in the output, which is where most people get stuck. Handles are assigned when a rule is added and stay with that rule until it is removed, but they are not reused or preserved across a reload, so list them again after every nft -f. There is no way to delete a rule by retyping it, so if you cannot find the handle, edit /etc/nftables.conf, remove the line, and reload the file with sudo nft -f /etc/nftables.conf.

Where are nftables rules stored?

Nowhere by default. The live ruleset exists only in the kernel, and sudo nft list ruleset is the only way to read it. The file the nftables service loads at boot is /etc/nftables.conf on Debian, Ubuntu, and Arch, and /etc/sysconfig/nftables.conf on RHEL, Rocky, and Fedora, where it usually just includes files from /etc/nftables/. Neither file updates itself: a rule you add with nft add rule is live immediately and absent from the file, and a rule you write in the file does nothing until you run sudo nft -f on it. Write the running ruleset back out with sudo nft -s list ruleset before you reboot.

Are nftables rules persistent after a reboot?

Only if you save them and enable the service. Two steps: write the ruleset to disk with sudo sh -c 'nft -s list ruleset > /etc/nftables.conf', then sudo systemctl enable --now nftables so the file is loaded at every boot. Check with systemctl is-enabled nftables. Add flush ruleset as the first line of the file so a reload replaces the ruleset instead of stacking a second copy of every rule on top of the running one. UFW and firewalld handle this for you, which is why a single server is often better off with one of those than with raw nft.

Can iptables and nftables run at the same time?

Technically yes, and it is a common way to get a firewall that behaves nothing like either ruleset. Both front ends feed the same netfilter hooks, so rules from both are evaluated and the first drop wins. On Debian 10, Ubuntu 20.04, RHEL 8 and anything newer, the iptables command is usually iptables-nft, a shim that writes nftables rules into tables named filter, nat and mangle in the ip family, so sudo nft list ruleset shows them alongside your own. Check which binary you have with sudo iptables -V: a version ending in (nf_tables) is the shim, (legacy) is the old xtables path, and the legacy one really does run a separate ruleset the nft command cannot see. Pick one tool and move everything to it.

Where does nftables log dropped packets?

To the kernel log, not to a file of its own. A rule with a log statement writes through the kernel's printk buffer, so it lands in journalctl -k (or /var/log/kern.log and /var/log/messages on systems with rsyslog). Give each rule a prefix, such as log prefix "nft-input-drop ", and follow it with sudo journalctl -k -f | grep nft-input-drop. A busy drop rule can flood the log, so put limit rate 5/minute in front of the log statement. For logs in their own file, use log group 2 instead, which hands packets to ulogd2 over nflog.

Related cheat sheets