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.
| Concept | Meaning |
| Table | A named container for chains. You name it — no predefined semantics. |
| Chain | An ordered list of rules. Base chains hook into netfilter; regular chains are called explicitly. |
| Rule | Match expressions + a verdict (accept, drop, jump, etc.). |
| Set | A collection of IP addresses, ports, or other values for O(1) membership lookups. |
| Map | A key→value lookup: match an IP, get back a port or verdict. |
| Verdict map | A map whose values are verdicts (accept, drop, jump chain). |
| Family | The address family the table operates on: ip, ip6, inet, arp, bridge, netdev. |
Family comparison
| Family | Covers | Replaces |
| ip | IPv4 only | iptables |
| ip6 | IPv6 only | ip6tables |
| inet | IPv4 + IPv6 together — use this for new rulesets | iptables + ip6tables |
| arp | ARP packets | arptables |
| bridge | Ethernet bridging | ebtables |
| netdev | Ingress/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
| Type | Valid hooks | Use for |
| filter | prerouting, input, forward, output, postrouting | Accepting / dropping packets |
| nat | prerouting, input, output, postrouting | SNAT, DNAT, masquerade |
| route | output | Rerouting packets (policy routing) |
Priority values
| Name | Value | Meaning |
| raw | -300 | Before conntrack — use for NOTRACK |
| mangle | -150 | Packet modification |
| dstnat | -100 | DNAT (prerouting) |
| filter | 0 | Standard filtering |
| security | 50 | Mandatory access control |
| srcnat | 100 | SNAT (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
| Expression | Matches |
| ip saddr 10.0.0.1 | IPv4 source address |
| ip daddr 192.168.1.0/24 | IPv4 destination CIDR |
| ip6 saddr ::1 | IPv6 source address |
| tcp dport 443 | TCP destination port |
| tcp dport { 80, 443, 8080 } | Multiple ports (anonymous set) |
| udp dport 53 | UDP destination port |
| iifname "eth0" | Incoming interface by name |
| oifname "eth1" | Outgoing interface by name |
| ct state established,related | Connection tracking state |
| ct state new | New connections only |
| icmp type echo-request | IPv4 ping request |
| icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, nd-router-solicit, nd-router-advert } | Required ICMPv6 types (NDP) |
| meta l4proto tcp | Layer-4 protocol (family-agnostic) |
| limit rate 5/minute | Rate limiting |
| limit rate over 100/second burst 200 packets | Rate 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
| Level | syslog equivalent |
| emerg | LOG_EMERG (0) |
| alert | LOG_ALERT (1) |
| crit | LOG_CRIT (2) |
| err | LOG_ERR (3) |
| warn | LOG_WARNING (4) |
| notice | LOG_NOTICE (5) |
| info | LOG_INFO (6) |
| debug | LOG_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
| Symptom | Cause & Fix |
| Rules lost on reboot | Ruleset not persisted — save with nft list ruleset > /etc/nftables.conf and enable the nftables systemd unit. |
| Syntax error on load | Run nft -c -f /etc/nftables.conf to validate without applying. |
| Rule added but traffic still blocked/allowed | Check rule order with nft -a list chain inet firewall input. Earlier rules win. |
| NAT not working | IP forwarding disabled — check sysctl net.ipv4.ip_forward. |
| FTP passive mode broken | Load nf_conntrack_ftp module and ensure ct state related accept is in the input chain. |
| IPv6 traffic bypasses rules | Use inet family tables — ip family tables only process IPv4. |
| Set element won't add | Verify the set type matches the value: ipv4_addr vs ip6_addr vs inet_service. |
| Dynamic set not expiring | Ensure flags timeout is declared on the set and a timeout value is set. |
| iptables and nftables conflict | Don'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
| Command | What it does |
| nft list ruleset | Show the full active ruleset |
| nft -a list ruleset | Show ruleset with rule handles |
| nft -c -f /etc/nftables.conf | Validate config file for syntax errors |
| nft -f /etc/nftables.conf | Apply ruleset atomically from file |
| nft list sets | List 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 input | Remove all rules from a chain |
| nft flush ruleset | Clear everything (use with caution) |
| nft monitor trace | Live packet tracing through the ruleset |
| systemctl reload nftables | Reload /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.