nftables: The Modern Linux Firewall Replacing iptables β Complete Guide (2026)
π― Key Takeaways
- Why nftables? What Was Wrong with iptables?
- Core Concepts: Tables, Chains, Rules, Sets
- Installation and Basic Commands
- Building a Complete Server Firewall
- Connection Tracking β The Heart of a Stateful Firewall
π Table of Contents
- Why nftables? What Was Wrong with iptables?
- Core Concepts: Tables, Chains, Rules, Sets
- Installation and Basic Commands
- Building a Complete Server Firewall
- Connection Tracking β The Heart of a Stateful Firewall
- NAT β Network Address Translation with nftables
- Sets and Maps β Advanced Traffic Classification
- Rate Limiting and DDoS Mitigation
- Logging
- nftables for Container and Docker Environments
- Migrating from iptables to nftables
- Integration with firewalld
- Persisting Rules Across Reboots
- Debugging and Troubleshooting
- Complete Reference β nft Cheat Sheet
- Conclusion
π Table of Contents
- Why nftables? What Was Wrong with iptables?
- Core Concepts: Tables, Chains, Rules, Sets
- Tables
- Chains
- Rules
- Sets
- Installation and Basic Commands
- Essential nft Commands
- Building a Complete Server Firewall
- Connection Tracking β The Heart of a Stateful Firewall
- NAT β Network Address Translation with nftables
- Source NAT / Masquerade (Internet Gateway)
- Destination NAT β Port Forwarding
- Sets and Maps β Advanced Traffic Classification
- Dynamic Sets (Automatically Updated)
- Verdict Maps β Route Traffic by Criteria
- Rate Limiting and DDoS Mitigation
- Logging
- nftables for Container and Docker Environments
- Migrating from iptables to nftables
- Automatic Migration Tool
- Common iptables β nftables Translations
- Integration with firewalld
- Persisting Rules Across Reboots
- Debugging and Troubleshooting
- Testing Without Locking Yourself Out
- Common Troubleshooting Commands
- Common Errors and Fixes
- Complete Reference β nft Cheat Sheet
- Conclusion
nftables is the modern Linux firewall framework that has replaced iptables, ip6tables, arptables, and ebtables β four separate tools β with a single unified system. It ships as the default firewall on Ubuntu 22.04+, RHEL 9, Rocky Linux 9, Debian 11+, and almost every modern distribution. If you are still writing iptables rules in 2026, you are using a legacy interface that is essentially a compatibility shim on top of nftables. This guide teaches you nftables from the ground up β the concepts, the syntax, and complete real-world rulesets for Linux servers.
Why nftables? What Was Wrong with iptables?
iptables has been the Linux firewall tool since the early 2000s. It worked, but it had serious design problems that became more painful as Linux infrastructure grew more complex:
- Four separate tools: iptables (IPv4), ip6tables (IPv6), arptables (ARP), ebtables (Ethernet bridging). Every rule had to be duplicated across tools for dual-stack support.
- No atomic rule updates: Changing iptables rules is a multi-step operation. Between steps, the firewall is in an inconsistent state β a race condition on production servers.
- Slow rule evaluation: iptables evaluates rules linearly, one by one. A chain with 500 rules checks all 500 for every packet. nftables uses hash maps and interval trees that scale to tens of thousands of rules with minimal overhead.
- No native sets: Blocking 10,000 IP addresses in iptables requires 10,000 individual rules. nftables has native sets β one rule references a set containing all 10,000 addresses.
- Kernel space duplication: iptables compiled matching logic into the kernel. nftables uses a virtual machine in the kernel β rules compile to bytecode, dramatically reducing kernel code complexity.
- No variables or named expressions: iptables rules were pure repetition. nftables supports variables, named sets, verdict maps, and reusable expressions.
nftables solves all of these. It is faster, cleaner, and dramatically more expressive. The iptables command still exists on modern systems but it now translates to nftables calls internally β iptables-nft is a compatibility layer, not the real thing.
Core Concepts: Tables, Chains, Rules, Sets
Before writing any rules, understand the data model. nftables has a clear hierarchy:
Table
βββ Chain
βββ Rule
βββ Match + Statement
Tables
A table is a namespace. It groups chains together. Every table has a family that determines what traffic it handles:
| Family | Handles | Replaces |
|---|---|---|
| ip | IPv4 packets | iptables |
| ip6 | IPv6 packets | ip6tables |
| inet | Both IPv4 and IPv6 | Both iptables + ip6tables |
| arp | ARP packets | arptables |
| bridge | Ethernet bridge packets | ebtables |
| netdev | Device ingress/egress (XDP-level) | New β no iptables equivalent |
For most server firewalls, you use inet β it handles both IPv4 and IPv6 with one set of rules. This alone eliminates the iptables/ip6tables duplication problem.
Chains
A chain contains rules and is attached to a hook in the networking stack. The hook determines when the chain is evaluated:
| Hook | When it fires | Common use |
|---|---|---|
| prerouting | All packets arriving at the interface, before routing decision | DNAT, connection tracking |
| input | Packets destined for this machine | Allow/block inbound connections |
| forward | Packets passing through (routing/NAT) | Router/gateway rules |
| output | Packets generated by this machine | Restrict outbound |
| postrouting | All packets after routing decision, before leaving | SNAT/masquerade |
Chains also have a policy β the default verdict when no rule matches. For an input chain on a server: policy drop means “deny everything not explicitly allowed” (allowlist). policy accept means “allow everything not explicitly blocked” (denylist). Servers should always use policy drop for input.
Chain priority controls evaluation order when multiple chains hook to the same point. Lower number = higher priority. Standard priorities: filter = 0, nat = 100, mangle = -150.
Rules
A rule is: match some packets + take an action (verdict). Rules are evaluated top to bottom within a chain. The first matching rule’s verdict is applied. If no rule matches, the chain policy applies.
# Rule anatomy:
# [match expressions] [statement/verdict]
tcp dport 22 accept
ip saddr 10.0.0.0/8 drop
ct state established accept
Sets
Sets are one of nftables’ most powerful features. A set is a collection of values (IP addresses, ports, interfaces, etc.) that rules can reference by name. Instead of 50 rules for 50 allowed ports, you have one rule referencing a set of 50 ports:
# Without sets β repetitive
tcp dport 22 accept
tcp dport 80 accept
tcp dport 443 accept
tcp dport 8080 accept
# With sets β clean and fast
tcp dport { 22, 80, 443, 8080 } accept
# Named sets (reusable across rules)
set allowed_ports {
type inet_service
elements = { 22, 80, 443, 8080 }
}
tcp dport @allowed_ports accept
Installation and Basic Commands
# nftables is pre-installed on all modern distributions
# Ubuntu/Debian
apt install nftables
# RHEL/Rocky Linux
dnf install nftables
# Enable and start the service (loads rules from /etc/nftables.conf at boot)
systemctl enable --now nftables
systemctl status nftables
# Check the nft version
nft --version
Essential nft Commands
# List everything (all tables, chains, rules, sets)
nft list ruleset
# List a specific table
nft list table inet filter
# List a specific chain
nft list chain inet filter input
# Add a rule (appends to chain)
nft add rule inet filter input tcp dport 80 accept
# Insert a rule at position 0 (top of chain)
nft insert rule inet filter input position 0 tcp dport 22 accept
# Delete a rule by handle number
nft list ruleset -a # -a shows handle numbers
nft delete rule inet filter input handle 5
# Flush (delete all rules in a chain)
nft flush chain inet filter input
# Flush entire table
nft flush table inet filter
# Delete a table
nft delete table inet filter
# Flush entire ruleset (delete everything)
nft flush ruleset
# Load rules from a file
nft -f /etc/nftables.conf
# Test a file without applying (syntax check)
nft -c -f /etc/nftables.conf
Building a Complete Server Firewall
This is a production-ready firewall for a Linux web server. It covers SSH, HTTP, HTTPS, ICMP, stateful connection tracking, rate limiting, and drops everything else. Save to /etc/nftables.conf.
nano /etc/nftables.conf
#!/usr/sbin/nft -f
# Clear everything first β ensures clean state on reload
flush ruleset
# Define variables for readability
define SSH_PORT = 22
define HTTP_PORT = 80
define HTTPS_PORT = 443
# Define a set of trusted management IPs
define MGMT_NETS = { 10.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12 }
table inet filter {
# Set of ports allowed from any source
set allowed_tcp {
type inet_service
elements = { 80, 443 }
}
chain input {
type filter hook input priority filter; policy drop;
# Accept traffic from loopback interface (required for local services)
iif "lo" accept comment "Accept loopback"
# Accept established and related connections (stateful firewall)
ct state established,related accept comment "Accept established connections"
# Drop invalid packets early (saves processing)
ct state invalid drop comment "Drop invalid packets"
# Accept ICMP β needed for ping, path MTU discovery, diagnostics
ip protocol icmp accept comment "Accept ICMP"
ip6 nexthdr icmpv6 accept comment "Accept ICMPv6"
# SSH β only from trusted management networks, rate limited
tcp dport $SSH_PORT ip saddr $MGMT_NETS \
limit rate 10/minute \
accept comment "SSH from management networks only"
# Web traffic β open to everyone
tcp dport @allowed_tcp accept comment "HTTP and HTTPS"
# Log and drop everything else
limit rate 5/minute log prefix "nft-input-drop: " level warn
drop
}
chain forward {
type filter hook forward priority filter; policy drop;
# This server does not route traffic β drop all forwarded packets
}
chain output {
type filter hook output priority filter; policy accept;
# Allow all outbound traffic (can be restricted further if needed)
}
}
# Test syntax before applying
nft -c -f /etc/nftables.conf
# Apply the ruleset
nft -f /etc/nftables.conf
# Verify it loaded correctly
nft list ruleset
# Restart to ensure it survives a reboot
systemctl restart nftables
Connection Tracking β The Heart of a Stateful Firewall
The most important rule in any firewall is the connection tracking rule:
ct state established,related accept
Without this rule, even if you allow port 22 inbound, the SSH response packets going outbound would be blocked (because the output chain would see them as new connections). Connection tracking understands that a packet is part of an existing conversation β it marks packets as:
- new β first packet of a new connection (SYN for TCP)
- established β part of an existing, tracked connection
- related β related to an existing connection (FTP data channel, ICMP error for a TCP connection)
- invalid β does not match any known connection, or has invalid flags/state
With stateful tracking, you only need to write rules for new inbound connections. All established traffic flows freely in both directions once the initial connection is permitted.
NAT β Network Address Translation with nftables
nftables handles all NAT types natively. NAT rules go in a separate table (or chain with nat type) to keep them separate from filter rules.
Source NAT / Masquerade (Internet Gateway)
table inet nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
# Masquerade all traffic going out eth0 (dynamic SNAT)
# Use this when your external IP can change (DHCP)
oif "eth0" masquerade
# Static SNAT β use when external IP is fixed
# oif "eth0" snat to 203.0.113.10
}
}
# Also enable IP forwarding in the kernel
echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.d/99-forwarding.conf
sysctl -p /etc/sysctl.d/99-forwarding.conf
Destination NAT β Port Forwarding
table inet nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
# Forward external port 80 β internal server 192.168.1.10:80
iif "eth0" tcp dport 80 dnat to 192.168.1.10
# Forward a different external port to internal port
# External 2222 β internal SSH on 192.168.1.20:22
iif "eth0" tcp dport 2222 dnat to 192.168.1.20:22
# Forward a port range
iif "eth0" tcp dport 8080-8090 dnat to 192.168.1.30
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oif "eth0" masquerade
}
}
Sets and Maps β Advanced Traffic Classification
Dynamic Sets (Automatically Updated)
Dynamic sets change at runtime β nftables can add and remove elements as traffic flows. This is how you implement basic IP blocking with auto-expiry:
table inet filter {
# A set that can be modified at runtime with 1-hour expiry
set blocklist {
type ipv4_addr
flags dynamic, timeout
timeout 1h
}
chain input {
type filter hook input priority filter; policy drop;
# Drop anything in the blocklist
ip saddr @blocklist drop comment "Drop blocked IPs"
# Everything else continues to other rules...
ct state established,related accept
# ... etc
}
}
# Add an IP to the blocklist at runtime (expires in 1 hour automatically)
nft add element inet filter blocklist { 198.51.100.42 }
# Check what is in the set
nft list set inet filter blocklist
# Remove an IP manually
nft delete element inet filter blocklist { 198.51.100.42 }
Verdict Maps β Route Traffic by Criteria
A verdict map is a lookup table that maps a value to a verdict. Instead of many rules, one rule does a table lookup:
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
# Map each port to its verdict β one rule handles all cases
tcp dport vmap {
22 : accept, # SSH
80 : accept, # HTTP
443 : accept, # HTTPS
23 : drop, # Telnet β explicitly drop
3389 : drop # RDP β explicitly drop
}
}
}
Rate Limiting and DDoS Mitigation
table inet filter {
# Track new connection attempts per source IP (for port knocking / brute force)
set ssh_ratelimit {
type ipv4_addr
flags dynamic
timeout 60s
}
chain input {
type filter hook input priority filter; policy drop;
# ICMP rate limiting β prevent ping floods
ip protocol icmp limit rate 10/second accept
ip protocol icmp drop
# SSH brute force protection using meter (per-source tracking)
tcp dport 22 ct state new \
meter ssh_meter { ip saddr limit rate 3/minute } \
accept
tcp dport 22 ct state new drop
# Global rate limit on new HTTP connections (DDoS mitigation)
tcp dport 80 ct state new limit rate 100/second accept
tcp dport 80 ct state new drop
# SYN flood protection β limit SYN packets
tcp flags syn tcp dport 80 limit rate 200/second accept
ct state established,related accept
}
}
Logging
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
# Log new SSH connections (before accepting)
tcp dport 22 ct state new log prefix "SSH-NEW: " level info
tcp dport { 22, 80, 443 } accept
# Log all drops (rate-limited to avoid flooding syslog)
limit rate 5/minute log prefix "INPUT-DROP: " level warn flags all
drop
}
}
# View nftables log entries
journalctl -k | grep "INPUT-DROP"
journalctl -k | grep "SSH-NEW"
# Or from dmesg
dmesg | grep "INPUT-DROP"
Log flags control what appears in the log message:
| Flag | What it logs |
|---|---|
flags all |
TCP flags (SYN, ACK, RST, etc.) |
flags ip options |
IP header options |
| (none) | Basic packet header info (addresses, ports) |
nftables for Container and Docker Environments
Docker on modern systems uses iptables-legacy for its NAT rules. This means Docker rules and nftables rules can coexist but do not directly interact. To use nftables on a Docker host:
# Check if Docker is using iptables or nftables
docker info | grep -i iptables
# Option 1: Let Docker manage its own chains (leave iptables-nft enabled)
# Add your rules to nftables, Docker manages docker-specific chains via iptables-nft
# Option 2: Disable Docker iptables management (advanced β manual setup required)
# Edit /etc/docker/daemon.json
{
"iptables": false
}
# For Podman (uses nftables natively on RHEL 9)
# Podman rootless containers do not require any special firewall configuration
firewall-cmd --add-masquerade --permanent # If using firewalld over nftables
Migrating from iptables to nftables
Automatic Migration Tool
# Save existing iptables rules
iptables-save > /tmp/iptables-backup.rules
ip6tables-save > /tmp/ip6tables-backup.rules
# Convert to nftables format automatically
iptables-restore-translate -f /tmp/iptables-backup.rules > /tmp/ruleset-from-iptables.nft
ip6tables-restore-translate -f /tmp/ip6tables-backup.rules >> /tmp/ruleset-from-iptables.nft
# Review the converted rules
cat /tmp/ruleset-from-iptables.nft
# Apply if it looks correct
nft -f /tmp/ruleset-from-iptables.nft
Common iptables β nftables Translations
| iptables | nftables equivalent |
|---|---|
-A INPUT -j DROP |
chain input { policy drop; } |
-A INPUT -m state --state ESTABLISHED -j ACCEPT |
ct state established accept |
-A INPUT -p tcp --dport 22 -j ACCEPT |
tcp dport 22 accept |
-A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT |
tcp dport { 80, 443 } accept |
-A INPUT -s 10.0.0.0/8 -j ACCEPT |
ip saddr 10.0.0.0/8 accept |
-A INPUT -m limit --limit 5/min -j LOG |
limit rate 5/minute log |
-t nat -A POSTROUTING -j MASQUERADE |
chain postrouting { ... masquerade } |
-t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-dest 192.168.1.10 |
tcp dport 80 dnat to 192.168.1.10 |
Integration with firewalld
On RHEL 9, Rocky Linux 9, and AlmaLinux 9, firewalld is the default interface. Since RHEL 8, firewalld uses nftables as its backend. You can use firewalld for simple zone-based rules and drop to raw nftables for advanced rules:
# firewalld (uses nftables backend)
firewall-cmd --state
firewall-cmd --get-active-zones
firewall-cmd --zone=public --list-all
# Open a port through firewalld
firewall-cmd --zone=public --add-port=8080/tcp --permanent
firewall-cmd --reload
# View the nftables rules that firewalld generates
nft list ruleset | grep -A 5 "firewalld"
# If you bypass firewalld with direct nftables rules, changes survive
# because nftables service loads /etc/nftables.conf independently
# BUT: firewalld and direct nftables rules can conflict β choose one approach
# To use pure nftables on RHEL (disable firewalld)
systemctl disable --now firewalld
systemctl enable --now nftables
Persisting Rules Across Reboots
# Method 1: nftables service (recommended β cross-distro)
# Edit /etc/nftables.conf with your complete ruleset
# The nftables service loads this file at boot
# Save current ruleset to the config file
nft list ruleset > /etc/nftables.conf
# Enable the service
systemctl enable nftables
# Method 2: Ubuntu β using /etc/nftables.conf
# Same as Method 1 β Ubuntu uses the nftables.service
# Verify rules survive a service restart
systemctl restart nftables
nft list ruleset
# Method 3: Include multiple files (for large rulesets)
# In /etc/nftables.conf:
# include "/etc/nftables.d/*.conf"
# Then split rules into:
# /etc/nftables.d/01-filter.conf
# /etc/nftables.d/02-nat.conf
# /etc/nftables.d/03-sets.conf
mkdir -p /etc/nftables.d
echo 'include "/etc/nftables.d/*.conf"' > /etc/nftables.conf
Debugging and Troubleshooting
Testing Without Locking Yourself Out
# CRITICAL: Before applying a new ruleset to a remote server,
# set a time-delayed restore in case you lock yourself out
# Schedule ruleset flush in 2 minutes (safety net)
echo "nft flush ruleset" | at now + 2 minutes
# Apply your new ruleset
nft -f /etc/nftables.conf
# Test connectivity (from another terminal or machine)
# If everything works, cancel the scheduled restore:
atrm $(atq | tail -1 | awk '{print $1}')
Common Troubleshooting Commands
# Check if nftables service is running
systemctl status nftables
# Syntax check without applying
nft -c -f /etc/nftables.conf
# List ruleset with handles (needed for deleting specific rules)
nft -a list ruleset
# Monitor packets matching rules in real time
nft monitor trace
# Enable rule tracing for a specific packet (debugging)
# Add a trace rule at the top of the chain
nft insert rule inet filter input meta nftrace set 1
nft monitor trace
# Remove the trace rule when done
# Check connection tracking table
conntrack -L 2>/dev/null || cat /proc/net/nf_conntrack | head -20
# Test if a port is being allowed (from the same server)
nmap -p 22,80,443 localhost
# Count packet matches per rule
nft list ruleset -a | grep -i counter
# Add counters to a specific rule to see match count
nft add rule inet filter input tcp dport 80 counter accept
nft list chain inet filter input # counter increments with each match
Common Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
Error: Could not process rule: Permission denied |
Not running as root | Use sudo nft … |
Error: unknown chain type 'nat' |
NAT module not loaded | modprobe nft_nat |
Error: Could not find set |
Set referenced before creation | Define set before referencing it in rules |
| Rules load but don’t persist | nftables service not enabled | systemctl enable nftables |
| Docker breaks after applying rules | Flushed Docker’s NAT chains | Use flush ruleset carefully; restart Docker |
| Can’t connect after applying rules | Blocked established sessions | Add ct state established,related accept early |
Complete Reference β nft Cheat Sheet
# ββ TABLE OPERATIONS ββββββββββββββββββββββββββββββββββ
nft list tables # List all tables
nft add table inet mytable # Create table
nft delete table inet mytable # Delete table
nft flush table inet mytable # Clear all chains/rules in table
# ββ CHAIN OPERATIONS ββββββββββββββββββββββββββββββββββ
nft list chains # List all chains
nft add chain inet mytable mychain \ # Create base chain
'{ type filter hook input priority 0; policy drop; }'
nft flush chain inet mytable mychain # Delete all rules in chain
nft delete chain inet mytable mychain # Delete chain (must be empty)
# ββ RULE OPERATIONS βββββββββββββββββββββββββββββββββββ
nft add rule inet filter input tcp dport 80 accept # Add rule (end)
nft insert rule inet filter input tcp dport 22 accept # Insert rule (start)
nft -a list chain inet filter input # Show rules with handles
nft delete rule inet filter input handle 5 # Delete rule by handle
# ββ SET OPERATIONS ββββββββββββββββββββββββββββββββββββ
nft add set inet filter myset '{ type ipv4_addr; }' # Create set
nft add element inet filter myset { 10.0.0.1 } # Add element
nft delete element inet filter myset { 10.0.0.1 } # Remove element
nft list set inet filter myset # List set contents
# ββ COMMON MATCH EXPRESSIONS ββββββββββββββββββββββββββ
# Source/destination address
ip saddr 192.168.1.0/24
ip daddr 10.0.0.1
ip6 saddr fd00::/8
# Protocol and ports
ip protocol tcp
tcp dport 22
tcp dport { 80, 443 }
tcp dport 8000-8999
udp dport 53
# Interface
iif "eth0" # Input interface
oif "eth1" # Output interface
iifname "eth0" # By name (slower but works with dynamic interfaces)
# Connection tracking state
ct state new
ct state established,related
ct state invalid
# TCP flags
tcp flags syn
tcp flags & (syn|ack) == syn
# Limit / rate
limit rate 10/second
limit rate 10/minute burst 50 packets
# ββ COMMON VERDICTS βββββββββββββββββββββββββββββββββββ
accept # Allow the packet
drop # Silently discard
reject # Discard and send ICMP error
log # Log to kernel log
counter # Count packets (add to any rule)
return # Return from current chain
jump mychain # Jump to named chain
goto mychain # Jump without returning
Conclusion
nftables is the present and future of Linux packet filtering. It is faster, cleaner, and more expressive than iptables, and it ships as the default on every major Linux distribution. The learning curve is real β the table/chain/rule model is more explicit than iptables β but once it clicks, you will never want to go back. Sets eliminate rule repetition, the unified inet family eliminates IPv4/IPv6 duplication, and atomic rule loading eliminates the race conditions that iptables always had. Start with the complete server firewall template in this guide, run nft list ruleset often to understand what you have, and use nft monitor trace when debugging. The investment pays off quickly.
Was this article helpful?
About Ramesh Sundararamaiah
Red Hat Certified Architect
Expert in Linux system administration, DevOps automation, and cloud infrastructure. Specializing in Red Hat Enterprise Linux, CentOS, Ubuntu, Docker, Ansible, and enterprise IT solutions.