nftables — Modern Linux Firewall Reference

The modern Linux packet-filtering framework — a unified, atomic, and expressive replacement for iptables, ip6tables, arptables, and ebtables, shipped as the default firewall backend since kernel 3.13.

IPv4 · IPv6 · ARP · Bridge · atomic ruleset updates · sets · maps · verdict maps
01 — What is nftables?

nftables replaces the entire netfilter legacy stack in a single framework. It introduces a cleaner syntax, atomic ruleset commits (no partial updates mid-reload), built-in sets and maps for high-performance lookups, and a single tool — nft — that handles IPv4, IPv6, ARP, and bridge filtering simultaneously.

Where iptables needed separate tools for IPv4, IPv6, ARP, and bridges — nftables handles all four in one unified ruleset loaded atomically.
ConceptMeaning
TableA named container for chains. You name it — no predefined semantics.
ChainAn ordered list of rules. Base chains hook into netfilter; regular chains are called explicitly.
RuleMatch expressions + a verdict (accept, drop, jump, etc.).
SetA collection of IP addresses, ports, or other values for O(1) membership lookups.
MapA key→value lookup: match an IP, get back a port or verdict.
Verdict mapA map whose values are verdicts (accept, drop, jump chain).
FamilyThe address family the table operates on: ip, ip6, inet, arp, bridge, netdev.
Family comparison
FamilyCoversReplaces
ipIPv4 onlyiptables
ip6IPv6 onlyip6tables
inetIPv4 + IPv6 together — use this for new rulesetsiptables + ip6tables
arpARP packetsarptables
bridgeEthernet bridgingebtables
netdevIngress/egress on a specific interface (earliest hook)
02 — Installation
Debian / Ubuntu
# nftables is pre-installed on Debian 10+ and Ubuntu 20.04+ sudo apt install nftables # Enable and start sudo systemctl enable --now nftables # Check version nft --version
RHEL / Fedora / Rocky / Alma
# nftables is the default backend for firewalld on RHEL 8+ sudo dnf install nftables # To use nft directly instead of firewalld: sudo systemctl disable --now firewalld sudo systemctl enable --now nftables
Arch Linux
sudo pacman -S nftables sudo systemctl enable --now nftables
Verify
nft --version nft list ruleset # show current rules (empty on fresh install)
ℹ  On Debian/Ubuntu 21+ and RHEL 8+, the iptables command is actually iptables-nft — a compatibility shim that translates iptables commands into nftables rules. You can run both during a migration, but pick one for new work.
03 — Core Concepts & Syntax

nftables configuration is written in a human-readable language that can be loaded from a file or typed interactively. Every object is scoped: family → table → chain → rule.

Creating tables and chains
# Create a table in the inet family (handles IPv4 + IPv6) nft add table inet filter # Create an input base chain (hooks into kernel's input path) nft add chain inet filter input \ '{ type filter hook input priority 0; policy drop; }' # Create an output base chain nft add chain inet filter output \ '{ type filter hook output priority 0; policy accept; }' # Create a forward base chain nft add chain inet filter forward \ '{ type filter hook forward priority 0; policy drop; }' # Create a regular chain (no hook — called explicitly via jump/goto) nft add chain inet filter allow-services
Chain types and hooks
TypeValid hooksUse for
filterprerouting, input, forward, output, postroutingAccepting / dropping packets
natprerouting, input, output, postroutingSNAT, DNAT, masquerade
routeoutputRerouting packets (policy routing)
Priority values
NameValueMeaning
raw-300Before conntrack — use for NOTRACK
mangle-150Packet modification
dstnat-100DNAT (prerouting)
filter0Standard filtering
security50Mandatory access control
srcnat100SNAT (postrouting)
Adding rules
# Append a rule to a chain nft add rule inet filter input tcp dport 22 accept # Insert at position 0 (top) nft insert rule inet filter input position 0 drop # Add with a handle (for later deletion/replacement) nft add rule inet filter input tcp dport 80 accept # Get the handle number: nft -a list chain inet filter input # Delete a rule by handle nft delete rule inet filter input handle 7 # Flush all rules from a chain nft flush chain inet filter input
Common match expressions
ExpressionMatches
ip saddr 10.0.0.1IPv4 source address
ip daddr 192.168.1.0/24IPv4 destination CIDR
ip6 saddr ::1IPv6 source address
tcp dport 443TCP destination port
tcp dport { 80, 443, 8080 }Multiple ports (anonymous set)
udp dport 53UDP destination port
iifname "eth0"Incoming interface by name
oifname "eth1"Outgoing interface by name
ct state established,relatedConnection tracking state
ct state newNew connections only
icmp type echo-requestIPv4 ping request
icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, nd-router-solicit, nd-router-advert }Required ICMPv6 types (NDP)
meta l4proto tcpLayer-4 protocol (family-agnostic)
limit rate 5/minuteRate limiting
limit rate over 100/second burst 200 packetsRate limit with burst
04 — Ruleset Files

Unlike iptables, nftables is designed to be loaded from a single configuration file. The entire ruleset is applied atomically — no partial states during reload. The standard location is /etc/nftables.conf.

Complete server firewall — /etc/nftables.conf
#!/usr/sbin/nft -f # Flush existing ruleset flush ruleset # ── TABLE ──────────────────────────────────────────────────────────── table inet firewall { # ── SETS ───────────────────────────────────────────────────────── # Whitelist — never blocked set allowed_hosts { type ipv4_addr flags interval elements = { 127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16 } } # Blocklist — permanently banned IPs set blocklist { type ipv4_addr flags interval elements = { } } # ── INPUT CHAIN ─────────────────────────────────────────────────── chain input { type filter hook input priority filter; policy drop; # Drop invalid packets immediately ct state invalid drop # Accept loopback iifname "lo" accept # Accept established / related ct state { established, related } accept # Drop blocklisted IPs ip saddr @blocklist drop # ICMPv4 — accept ping (rate-limited) and essential types icmp type echo-request limit rate 5/second accept icmp type { echo-reply, destination-unreachable, time-exceeded, parameter-problem } accept # ICMPv6 — required for IPv6 operation icmpv6 type { echo-request, echo-reply, destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept # SSH — from anywhere (restrict to allowed_hosts for hardened setups) tcp dport 22 ct state new accept # HTTP / HTTPS tcp dport { 80, 443 } ct state new accept # Log and drop everything else limit rate 5/minute log prefix "nft-INPUT-DROP: " level warn drop } # ── FORWARD CHAIN ───────────────────────────────────────────────── chain forward { type filter hook forward priority filter; policy drop; ct state { established, related } accept } # ── OUTPUT CHAIN ────────────────────────────────────────────────── chain output { type filter hook output priority filter; policy accept; } }
Load the ruleset
# Test for syntax errors without applying nft -c -f /etc/nftables.conf # Apply (atomically — replaces the entire current ruleset) nft -f /etc/nftables.conf # Enable auto-load on boot sudo systemctl enable nftables
List the active ruleset
# Full ruleset nft list ruleset # Specific table nft list table inet firewall # Specific chain nft list chain inet firewall input # With rule handles (needed for deletion) nft -a list ruleset
05 — Common Service Rules
SSH
chain input { # Allow SSH from anywhere tcp dport 22 ct state new accept # Allow SSH from a specific subnet only ip saddr 203.0.113.0/24 tcp dport 22 ct state new accept # Custom SSH port tcp dport 2222 ct state new accept }
HTTP & HTTPS
chain input { # HTTP and HTTPS tcp dport { 80, 443 } ct state new accept # HTTPS only (redirect HTTP elsewhere or reject) tcp dport 443 ct state new accept tcp dport 80 reject with tcp reset }
FTP (with conntrack helper)
# Load FTP conntrack helper — add to /etc/modules-load.d/nf_conntrack_ftp.conf # nf_conntrack_ftp # In /etc/nftables.conf table inet firewall { chain input { # FTP control channel tcp dport 21 ct state new accept # Accept RELATED connections (passive mode data channel) ct state related accept # FTPS tcp dport { 989, 990 } ct state new accept } } # Enable the conntrack FTP helper echo "nf_conntrack_ftp" | sudo tee /etc/modules-load.d/nf_conntrack_ftp.conf sudo modprobe nf_conntrack_ftp
SMTP / Mail
chain input { # SMTP (relay) tcp dport 25 ct state new accept # Submission (authenticated clients) tcp dport 587 ct state new accept # SMTPS tcp dport 465 ct state new accept }
IMAP & POP3
chain input { # IMAP and IMAPS tcp dport { 143, 993 } ct state new accept # POP3 and POP3S tcp dport { 110, 995 } ct state new accept }
DNS
chain input { # Accept DNS queries (UDP + TCP) udp dport 53 accept tcp dport 53 accept }
MySQL / MariaDB & PostgreSQL
chain input { # MySQL — app server only ip saddr 10.0.0.20 tcp dport 3306 ct state new accept tcp dport 3306 drop # block all others # PostgreSQL — app subnet only ip saddr 10.0.0.0/24 tcp dport 5432 ct state new accept tcp dport 5432 drop }
NTP
chain output { # Allow outbound NTP udp dport 123 accept } chain input { # NTP server — accept client queries udp dport 123 accept }
06 — Sets & Maps

Sets are one of nftables' most powerful features — they allow O(1) membership testing against large lists of IPs, ports, or other values, directly inside a single rule.

Named sets
table inet firewall { # Static set of admin IPs set admin_ips { type ipv4_addr elements = { 203.0.113.5, 198.51.100.10 } } # Set of allowed web ports set web_ports { type inet_service # port numbers elements = { 80, 443, 8080, 8443 } } # Dynamic set — elements added/removed at runtime by other rules set bruteforce_ssh { type ipv4_addr flags dynamic, timeout timeout 5m # auto-expire after 5 minutes } chain input { # Use the sets ip saddr @admin_ips tcp dport 22 ct state new accept tcp dport @web_ports ct state new accept } }
Add / remove set elements at runtime
# Add an IP to a named set nft add element inet firewall blocklist { 203.0.113.99 } # Add a CIDR nft add element inet firewall blocklist { 198.51.100.0/24 } # Remove an IP from a set nft delete element inet firewall blocklist { 203.0.113.99 } # List set contents nft list set inet firewall blocklist
Maps — key → value lookups
table inet firewall { # Map external port → internal host:port (for DNAT) map port_forward { type inet_service : ipv4_addr . inet_service elements = { 80 : 192.168.1.10 . 80, 443 : 192.168.1.10 . 443, 8080 : 192.168.1.20 . 8080 } } chain prerouting { type nat hook prerouting priority dstnat; # DNAT using the map dnat ip addr . port to tcp dport map @port_forward } }
Verdict maps
table inet firewall { # Map source IP to a verdict map src_policy { type ipv4_addr : verdict elements = { 10.0.0.1 : accept, 203.0.113.99 : drop, 198.51.100.5 : jump log-and-drop } } chain input { # Apply the verdict map — one rule handles N policies ip saddr vmap @src_policy } }
07 — NAT & Routing

NAT in nftables uses chains of type nat with prerouting/postrouting hooks. Enable kernel IP forwarding first.

Enable IP forwarding
# Permanent — /etc/sysctl.d/99-forward.conf net.ipv4.ip_forward = 1 net.ipv6.conf.all.forwarding = 1 sysctl -p /etc/sysctl.d/99-forward.conf
Masquerade (NAT / internet sharing)
table ip nat { chain postrouting { type nat hook postrouting priority srcnat; # Masquerade all traffic leaving eth0 oifname "eth0" masquerade } } # Also allow forwarding in the filter table table inet firewall { chain forward { type filter hook forward priority filter; policy drop; iifname "eth1" oifname "eth0" ct state new accept ct state { established, related } accept } }
SNAT — fixed source IP
table ip nat { chain postrouting { type nat hook postrouting priority srcnat; oifname "eth0" snat to 203.0.113.5 } }
DNAT — port forwarding
table ip nat { chain prerouting { type nat hook prerouting priority dstnat; # Forward port 80 to internal web server iifname "eth0" tcp dport 80 dnat to 192.168.1.10:80 # Forward external 8443 to internal 443 iifname "eth0" tcp dport 8443 dnat to 192.168.1.20:443 } }
Hairpin NAT (LAN → public IP → LAN)
table ip nat { chain postrouting { type nat hook postrouting priority srcnat; # Masquerade for LAN traffic being forwarded back into LAN ip saddr 192.168.1.0/24 ip daddr 192.168.1.0/24 masquerade oifname "eth0" masquerade } }
08 — Rate Limiting & DDoS Mitigation

nftables provides native limit, meter, and dynamic set primitives for per-IP rate limiting — no external modules required.

Global rate limiting
chain input { # Allow at most 10 new SSH connections per minute globally tcp dport 22 ct state new limit rate 10/minute accept tcp dport 22 ct state new drop # Rate-limit ICMP ping icmp type echo-request limit rate 5/second burst 10 packets accept icmp type echo-request drop }
Per-source-IP rate limiting (meters)
table inet firewall { chain input { # Drop IPs that send more than 5 new SSH connections per minute tcp dport 22 ct state new \ meter ssh_meter { ip saddr timeout 1m limit rate over 5/minute } \ drop tcp dport 22 ct state new accept } }
Dynamic ban set — auto-expire brute-force IPs
table inet firewall { # Dynamic set: IPs auto-expire after 5 minutes set ssh_banned { type ipv4_addr flags dynamic, timeout timeout 5m } chain input { # Step 1: drop if already in the banned set ip saddr @ssh_banned drop # Step 2: add to set if over threshold (3 new connections in 30s) tcp dport 22 ct state new \ add @ssh_banned { ip saddr timeout 5m limit rate over 3/minute } \ drop # Step 3: allow the rest tcp dport 22 ct state new accept } }
SYN flood & port-scan protection
chain input { # Drop packets with invalid TCP flags tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 drop # NULL scan tcp flags & (fin|syn) == (fin|syn) drop # Invalid combo tcp flags & (syn|rst) == (syn|rst) drop # Invalid combo tcp flags & (fin|rst) == (fin|rst) drop # Invalid combo tcp flags & (fin|ack) == fin drop # FIN without ACK tcp flags & (urg|ack) == urg drop # URG without ACK # Drop XMAS scan (all flags set) tcp flags == (fin|syn|rst|psh|ack|urg) drop # Drop invalid connection state ct state invalid drop }
09 — Logging

nftables supports the log statement, which can output to the kernel log (dmesg/syslog) or to nfnetlink (for userspace capture with ulogd2). Unlike iptables, logging is a statement — it doesn't need a separate rule.

Basic kernel logging
chain input { # Log and drop — both actions in one rule tcp dport 22 ct state new \ limit rate 3/minute \ log prefix "nft-SSH-NEW: " level info \ accept # Log drops at the end of chain limit rate 5/minute log prefix "nft-INPUT-DROP: " level warn drop }
Log levels
Levelsyslog equivalent
emergLOG_EMERG (0)
alertLOG_ALERT (1)
critLOG_CRIT (2)
errLOG_ERR (3)
warnLOG_WARNING (4)
noticeLOG_NOTICE (5)
infoLOG_INFO (6)
debugLOG_DEBUG (7)
View log output
# Kernel ring buffer dmesg | grep nft # Syslog / journald journalctl -k | grep "nft-" grep "nft-" /var/log/syslog
Structured logging with ulogd2
# Install ulogd2 sudo apt install ulogd2 # Use group-based log statement in nftables chain input { drop log group 100 # sends to ulogd2 group 100 }
10 — iptables Compatibility

nftables ships an iptables-compatible front-end — iptables-nft — so existing scripts continue to work during migration. You can also translate iptables rules automatically.

iptables-nft shim
# On Debian/Ubuntu, check which iptables is in use update-alternatives --list iptables # Switch to nftables backend sudo update-alternatives --set iptables /usr/sbin/iptables-nft sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-nft
Translate existing iptables rules to nftables syntax
# Install translation tools sudo apt install iptables-nftables-compat # Translate saved iptables rules iptables-save | iptables-restore-translate -f /dev/stdin > /etc/nftables.conf # Translate a single command (preview) iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT # Output: nft add rule ip filter INPUT tcp dport 22 counter accept
✔  Use iptables-translate to convert rules one at a time as you learn nftables syntax. It's the fastest way to understand the mapping between the two systems.
11 — Managing Rules at Runtime

nft supports interactive rule management without a full reload — useful for quick bans, debugging, and temporary changes.

Interactive management
# List full ruleset nft list ruleset # List with handles (needed to delete/replace rules) nft -a list ruleset # Add a rule to block an IP immediately nft add rule inet firewall input ip saddr 203.0.113.99 drop # Delete a rule by handle nft delete rule inet firewall input handle 12 # Flush a chain nft flush chain inet firewall input # Flush an entire table nft flush table inet firewall # Delete a table nft delete table inet firewall
Atomic batch update (preferred for production)
# Write a partial update file and apply atomically cat << 'EOF' | nft -f - add element inet firewall blocklist { 203.0.113.99, 198.51.100.0/24 } EOF # Full atomic replace nft -f /etc/nftables.conf
Persist current ruleset
# Export current in-memory ruleset to file nft list ruleset > /etc/nftables.conf # Reload from file nft -f /etc/nftables.conf # Reload via systemd sudo systemctl reload nftables # Restart (full stop + start) sudo systemctl restart nftables
12 — Troubleshooting
SymptomCause & Fix
Rules lost on rebootRuleset not persisted — save with nft list ruleset > /etc/nftables.conf and enable the nftables systemd unit.
Syntax error on loadRun nft -c -f /etc/nftables.conf to validate without applying.
Rule added but traffic still blocked/allowedCheck rule order with nft -a list chain inet firewall input. Earlier rules win.
NAT not workingIP forwarding disabled — check sysctl net.ipv4.ip_forward.
FTP passive mode brokenLoad nf_conntrack_ftp module and ensure ct state related accept is in the input chain.
IPv6 traffic bypasses rulesUse inet family tables — ip family tables only process IPv4.
Set element won't addVerify the set type matches the value: ipv4_addr vs ip6_addr vs inet_service.
Dynamic set not expiringEnsure flags timeout is declared on the set and a timeout value is set.
iptables and nftables conflictDon't mix backends. Choose one. iptables-nft writes to nftables internally but is separate from native nft tables.
Diagnostic commands
# Validate config without applying nft -c -f /etc/nftables.conf # Trace a packet (requires nftables 0.9.1+) nft add rule inet firewall input tcp dport 22 meta nftrace set 1 nft monitor trace # Check conntrack table conntrack -L cat /proc/net/nf_conntrack # Monitor ruleset changes in real time nft monitor # List all sets and their contents nft list sets # Check kernel module is loaded lsmod | grep nf_conntrack
13 — Complete Production Example

A full /etc/nftables.conf for a typical web + mail server with sets, rate-limiting, NAT, and logging — ready to adapt and deploy.

#!/usr/sbin/nft -f # Production nftables ruleset # Tested on Debian 12 / Ubuntu 24.04 / RHEL 9 flush ruleset table inet firewall { # ── SETS ──────────────────────────────────────────────────────── set whitelist { type ipv4_addr flags interval elements = { 127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16 } } set blocklist { type ipv4_addr flags interval # Add manually: nft add element inet firewall blocklist { x.x.x.x } elements = { } } set ssh_ratelimit { type ipv4_addr flags dynamic, timeout timeout 10m } # ── INPUT ──────────────────────────────────────────────────────── chain input { type filter hook input priority filter; policy drop; ct state invalid drop comment "drop invalid packets" iifname "lo" accept comment "loopback" ct state { established, related } accept comment "return traffic" ip saddr @whitelist accept comment "always allow trusted hosts" ip saddr @blocklist drop comment "drop banned IPs" # ICMPv4 icmp type echo-request limit rate 5/second accept icmp type { echo-reply, destination-unreachable, time-exceeded, parameter-problem } accept # ICMPv6 (required for IPv6 operation) icmpv6 type { echo-request, echo-reply, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, packet-too-big, time-exceeded, parameter-problem, destination-unreachable } accept # SSH — rate limited per source IP tcp dport 22 ct state new \ add @ssh_ratelimit { ip saddr timeout 10m limit rate over 5/minute } \ drop tcp dport 22 ct state new log prefix "SSH-NEW: " level info accept # HTTP / HTTPS tcp dport { 80, 443 } ct state new accept # SMTP / Submission / SMTPS tcp dport { 25, 465, 587 } ct state new accept # IMAP / IMAPS / POP3S tcp dport { 143, 993, 995 } ct state new accept # DNS (if this host is a resolver) # udp dport 53 accept # tcp dport 53 accept # Log and drop remainder limit rate 5/minute \ log prefix "nft-INPUT-DROP: " level warn drop } # ── FORWARD ────────────────────────────────────────────────────── chain forward { type filter hook forward priority filter; policy drop; ct state { established, related } accept # Add FORWARD rules for routing/NAT scenarios here } # ── OUTPUT ─────────────────────────────────────────────────────── chain output { type filter hook output priority filter; policy accept; # Outbound is unrestricted; tighten below if needed } } # ── NAT TABLE (uncomment if this host does NAT / port-forwarding) ── # table ip nat { # chain prerouting { # type nat hook prerouting priority dstnat; # iifname "eth0" tcp dport 80 dnat to 192.168.1.10:80 # iifname "eth0" tcp dport 443 dnat to 192.168.1.10:443 # } # chain postrouting { # type nat hook postrouting priority srcnat; # oifname "eth0" masquerade # } # }
14 — Quick Reference
CommandWhat it does
nft list rulesetShow the full active ruleset
nft -a list rulesetShow ruleset with rule handles
nft -c -f /etc/nftables.confValidate config file for syntax errors
nft -f /etc/nftables.confApply ruleset atomically from file
nft list setsList all named sets and contents
nft add element inet firewall blocklist { <IP> }Add an IP to a named set
nft delete element inet firewall blocklist { <IP> }Remove an IP from a named set
nft delete rule inet firewall input handle <n>Delete a specific rule by handle
nft flush chain inet firewall inputRemove all rules from a chain
nft flush rulesetClear everything (use with caution)
nft monitor traceLive packet tracing through the ruleset
systemctl reload nftablesReload /etc/nftables.conf without full restart
✔  Start with the inet family — one table handles both IPv4 and IPv6 simultaneously, eliminating the dual-ruleset maintenance burden of iptables + ip6tables.