nftables is the modern packet classification framework in the Linux kernel, designed as the successor to the legacy iptables, ip6tables, arptables, and ebtables tools. Since its introduction in Linux kernel 3.13, nftables has become the default firewalling backend on most major Linux distributions, including Debian 10+, Ubuntu 20.10+, RHEL 8+, and Fedora 18+. This guide provides a comprehensive walkthrough of nftables concepts, syntax, and practical configurations for securing Linux servers, including NAT, rate limiting, sets, logging, and migration from iptables.
Prerequisites
Before you begin, make sure you have:
- Ubuntu Server 20.04, 22.04, or 24.04 (or any Linux distribution with kernel 3.13 or later)
- Terminal access with sudo privileges
- SSH access to the server (if configuring remotely)
- Basic understanding of TCP/UDP ports, IP addressing, and networking concepts
- Familiarity with iptables concepts (helpful but not required)
What Is nftables?
nftables is a subsystem of the Linux kernel that provides packet filtering, network address translation (NAT), packet mangling, and stateful traffic classification. It replaces the legacy Netfilter tools (iptables, ip6tables, arptables, ebtables) with a single, unified framework managed through the nft command-line utility.
Key advantages of nftables over iptables:
- Unified tool — one
nftcommand replaces iptables, ip6tables, arptables, and ebtables - Cleaner syntax — more readable, consistent rule format with proper grammar
- Better performance — native set and map data structures for efficient matching against large lists of IPs, ports, or interfaces
- Atomic rule replacement — load entire rulesets atomically, preventing gaps during updates
- No pre-defined tables or chains — you create only what you need, reducing overhead
- Built-in dual-stack support — the
inetfamily handles both IPv4 and IPv6 in a single table
nftables vs iptables
Understanding the key differences helps when transitioning:
| Feature | iptables | nftables |
|---|---|---|
| Command | iptables, ip6tables, arptables, ebtables | nft (single tool) |
| Address families | Separate tool per family | Unified inet family for IPv4/IPv6 |
| Syntax | Flag-based (-A, -j, -p) | Structured grammar (add rule ... accept) |
| Sets | Requires ipset extension | Native sets and maps built-in |
| Rule updates | Linear rule insertion/deletion | Atomic ruleset replacement via -f |
| Pre-built chains | Mandatory built-in chains (INPUT, OUTPUT, FORWARD) | No defaults; you create tables and chains as needed |
| Performance | Linear rule evaluation | Optimized set lookups, binary expressions |
| Kernel interface | xtables (one per tool) | nf_tables (single unified API) |
Installing nftables on Ubuntu
On Ubuntu 22.04 and later, nftables is available in the default repositories. Install it:
sudo apt update
sudo apt install nftables
Enable and start the service so rules persist across reboots:
sudo systemctl enable nftables
sudo systemctl start nftables
Verify the installation:
nft --version
Expected output:
nftables v1.0.6 (Lester Gooch #4)
Check the current ruleset:
sudo nft list ruleset
If no rules have been configured, the output will be empty. This is expected — unlike iptables, nftables starts with no tables or chains.
Core Concepts
nftables organizes packet filtering into a hierarchy of tables, chains, and rules. Understanding this hierarchy is essential.
Address Families
Each table belongs to an address family that determines which traffic it processes:
| Family | Description |
|---|---|
ip | IPv4 traffic only |
ip6 | IPv6 traffic only |
inet | Both IPv4 and IPv6 (recommended) |
arp | ARP protocol |
bridge | Bridge-level traffic |
netdev | Ingress traffic on a specific interface |
For most server configurations, use the inet family to handle both IPv4 and IPv6 with a single set of rules.
Tables
Tables are containers for chains and sets. They have a name and belong to an address family. Unlike iptables, there are no pre-defined tables — you create them as needed:
sudo nft add table inet filter
sudo nft add table inet nat
Chains
Chains hold rules and define when those rules are evaluated. There are two types:
- Base chains — attached to a Netfilter hook (input, output, forward, prerouting, postrouting). These are entry points for packet processing.
- Regular chains — not attached to a hook; used as jump targets for organizing rules.
Base chains require a type, hook, and priority:
sudo nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }
Rules
Rules are the actual matching and action statements within a chain. They consist of expressions (match criteria) and a verdict (accept, drop, reject, etc.):
sudo nft add rule inet filter input tcp dport 22 accept
Creating Tables and Chains
Let us build a complete firewall configuration step by step. Start by creating the table:
sudo nft add table inet filter
Create the base chains with default policies:
# Input chain: drop all incoming traffic by default
sudo nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }
# Forward chain: drop forwarded traffic by default
sudo nft add chain inet filter forward { type filter hook forward priority 0 \; policy drop \; }
# Output chain: allow all outgoing traffic by default
sudo nft add chain inet filter output { type filter hook output priority 0 \; policy accept \; }
List your tables and chains to confirm:
sudo nft list tables
sudo nft list chains
Writing Rules
Accept Established and Related Connections
The first rules in your input chain should accept traffic belonging to established or related connections. This ensures that return traffic for your outgoing connections is not blocked:
sudo nft add rule inet filter input ct state established,related accept
Allow Loopback Traffic
Local services communicate through the loopback interface. Always allow it:
sudo nft add rule inet filter input iif lo accept
Drop Invalid Connections
Discard packets that are not part of any known connection:
sudo nft add rule inet filter input ct state invalid drop
Allow ICMP
Allow ping and other ICMP messages for diagnostics:
sudo nft add rule inet filter input ip protocol icmp accept
sudo nft add rule inet filter input ip6 nexthdr icmpv6 accept
Allow Specific Services
Accept traffic on specific ports:
# Allow SSH
sudo nft add rule inet filter input tcp dport 22 accept
# Allow HTTP and HTTPS
sudo nft add rule inet filter input tcp dport { 80, 443 } accept
# Allow a custom application port
sudo nft add rule inet filter input tcp dport 8080 accept
# Allow a UDP port (e.g., WireGuard)
sudo nft add rule inet filter input udp dport 51820 accept
Restrict by Source Address
Allow SSH only from a trusted subnet:
sudo nft add rule inet filter input ip saddr 192.168.1.0/24 tcp dport 22 accept
Reject Instead of Drop
Dropping sends no response; rejecting sends an ICMP error back. Add a reject as the final rule for cleaner network behavior:
sudo nft add rule inet filter input reject with icmpx type port-unreachable
View the Complete Ruleset
sudo nft list ruleset
Example output:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif "lo" accept
ct state invalid drop
ip protocol icmp accept
ip6 nexthdr ipv6-icmp accept
tcp dport 22 accept
tcp dport { 80, 443 } accept
reject with icmpx type port-unreachable
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
NAT Configuration
nftables handles NAT through a dedicated table with prerouting and postrouting chains.
Create the NAT Table and Chains
sudo nft add table inet nat
sudo nft add chain inet nat prerouting { type nat hook prerouting priority -100 \; }
sudo nft add chain inet nat postrouting { type nat hook postrouting priority 100 \; }
Source NAT (Masquerade)
Masquerade outgoing traffic from an internal network through the server’s public interface. This is common for routers and VPN gateways:
sudo nft add rule inet nat postrouting oif "eth0" masquerade
For a specific source subnet:
sudo nft add rule inet nat postrouting ip saddr 10.0.0.0/24 oif "eth0" masquerade
Static SNAT
If your server has a static public IP and you want to explicitly define the source address:
sudo nft add rule inet nat postrouting ip saddr 10.0.0.0/24 oif "eth0" snat to 203.0.113.1
Destination NAT (Port Forwarding)
Forward incoming traffic on a port to an internal host:
# Forward port 8080 to internal server 10.0.0.50:80
sudo nft add rule inet nat prerouting iif "eth0" tcp dport 8080 dnat to 10.0.0.50:80
Do not forget to allow the forwarded traffic in your filter table:
sudo nft add rule inet filter forward ip daddr 10.0.0.50 tcp dport 80 accept
sudo nft add rule inet filter forward ct state established,related accept
Enable IP Forwarding
NAT requires IP forwarding to be enabled in the kernel:
sudo sysctl -w net.ipv4.ip_forward=1
Make it persistent:
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.d/99-nftables.conf
sudo sysctl -p /etc/sysctl.d/99-nftables.conf
Rate Limiting and Connection Tracking
Rate Limiting Incoming Connections
Protect against brute-force attacks by limiting connection rates. For example, limit SSH to 5 new connections per minute per source IP:
sudo nft add rule inet filter input tcp dport 22 ct state new limit rate 5/minute accept
For a burst allowance (allow short bursts above the rate):
sudo nft add rule inet filter input tcp dport 22 ct state new limit rate 5/minute burst 10 packets accept
Per-Source Rate Limiting with Meters
Meters (formerly called dynamic sets) allow per-source-IP rate limiting:
sudo nft add rule inet filter input tcp dport 22 ct state new meter ssh-rate { ip saddr limit rate 3/minute burst 5 packets } accept
This limits each individual source IP to 3 new SSH connections per minute, with a burst of 5 packets.
Connection Tracking States
nftables uses the conntrack subsystem for stateful filtering. Available states:
| State | Description |
|---|---|
new | First packet of a new connection |
established | Part of an already established connection |
related | Related to an established connection (e.g., FTP data channel) |
invalid | Not associated with any known connection |
untracked | Explicitly bypassed by conntrack |
Limiting ICMP Flood
sudo nft add rule inet filter input ip protocol icmp limit rate 10/second burst 20 packets accept
sudo nft add rule inet filter input ip protocol icmp drop
Sets and Maps
Sets and maps are one of the most powerful features in nftables, enabling efficient matching against large groups of values without writing individual rules.
Anonymous Sets
Anonymous sets are defined inline within a rule:
sudo nft add rule inet filter input tcp dport { 22, 80, 443, 8080 } accept
Named Sets
Named sets are defined separately and can be updated dynamically without modifying the rules that reference them:
# Create a named set for allowed TCP ports
sudo nft add set inet filter allowed_ports { type inet_service \; }
# Add elements to the set
sudo nft add element inet filter allowed_ports { 22, 80, 443, 8080 }
# Use the set in a rule
sudo nft add rule inet filter input tcp dport @allowed_ports accept
Update the set without touching the rule:
# Add a new port
sudo nft add element inet filter allowed_ports { 3000 }
# Remove a port
sudo nft delete element inet filter allowed_ports { 8080 }
IP Address Sets
# Create a set for blocked IPs
sudo nft add set inet filter blocked_ips { type ipv4_addr \; }
# Populate the set
sudo nft add element inet filter blocked_ips { 203.0.113.100, 198.51.100.50, 192.0.2.75 }
# Drop traffic from blocked IPs
sudo nft add rule inet filter input ip saddr @blocked_ips drop
Sets with Timeouts (Auto-Expiring Entries)
Create sets where entries automatically expire:
sudo nft add set inet filter temp_block { type ipv4_addr \; timeout 1h \; }
sudo nft add element inet filter temp_block { 203.0.113.100 timeout 30m }
sudo nft add rule inet filter input ip saddr @temp_block drop
Maps for Verdict Lookups
Maps associate a key with a verdict, enabling efficient per-value decisions:
# Create a verdict map for port-based decisions
sudo nft add map inet filter port_policy { type inet_service : verdict \; }
sudo nft add element inet filter port_policy { 22 : accept, 80 : accept, 443 : accept, 23 : drop }
# Use the map in a rule
sudo nft add rule inet filter input tcp dport vmap @port_policy
Concatenated Sets
Match on multiple fields simultaneously:
# Create a set matching IP + port combinations
sudo nft add set inet filter allowed_access { type ipv4_addr . inet_service \; }
sudo nft add element inet filter allowed_access { 10.0.1.20 . 3306, 10.0.1.21 . 5432 }
sudo nft add rule inet filter input ip saddr . tcp dport @allowed_access accept
Logging Rules
nftables provides flexible logging for monitoring and debugging firewall rules.
Basic Logging
Log packets before dropping them:
sudo nft add rule inet filter input tcp dport 23 log prefix \"Telnet attempt: \" drop
Log with Rate Limiting
Prevent log flooding by limiting the logging rate:
sudo nft add rule inet filter input ct state invalid log prefix \"Invalid packet: \" limit rate 5/minute drop
Log Levels
nftables supports standard syslog levels:
sudo nft add rule inet filter input tcp dport 22 ct state new log prefix \"SSH connection: \" level info accept
Available levels: emerg, alert, crit, err, warn, notice, info, debug.
Log Groups (for nflog)
Send logs to a userspace program using log groups:
sudo nft add rule inet filter input tcp dport 80 log group 1 accept
You can then capture these with ulogd2 or other userspace logging daemons.
Viewing Logs
nftables logs are written to the kernel log. View them with:
sudo dmesg | grep "nft"
sudo journalctl -k --grep="Telnet attempt"
For persistent logging, configure rsyslog to write nftables messages to a dedicated file:
# /etc/rsyslog.d/10-nftables.conf
:msg, contains, "nft" /var/log/nftables.log
Restart rsyslog:
sudo systemctl restart rsyslog
Migrating from iptables
If you have existing iptables rules, nftables provides tools to ease the migration.
Using iptables-translate
The iptables-translate command converts individual iptables rules to nft syntax:
iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
Output:
nft add rule ip filter INPUT tcp dport 22 counter accept
Translating a Complete Ruleset
Export your entire iptables configuration and translate it:
iptables-save > /tmp/iptables-rules.txt
iptables-restore-translate -f /tmp/iptables-rules.txt > /tmp/nftables-rules.nft
Review the translated file and then load it:
sudo nft -f /tmp/nftables-rules.nft
Checking the Compatibility Layer
Modern Ubuntu uses iptables-nft as the default backend. Check which backend your system uses:
update-alternatives --query iptables
Or:
iptables --version
If you see nf_tables in the output, your iptables commands are already being translated to nftables internally.
Migration Best Practices
- Export existing rules — save your current iptables rules before starting
- Translate and review — use
iptables-restore-translateand carefully review the output - Test in a non-production environment — apply the translated rules on a test server first
- Flush old rules — once nft rules are confirmed, flush iptables rules with
iptables -F - Disable iptables service — stop and disable the iptables service to avoid conflicts
- Enable nftables service — ensure the nftables service is enabled for persistence
Loading Rulesets from Files
For production environments, managing your ruleset in a file is the recommended approach. Here is a complete example configuration:
#!/usr/sbin/nft -f
# Flush existing rules
flush ruleset
# Define variables
define WAN_IF = eth0
define LAN_IF = eth1
define LAN_NET = 10.0.0.0/24
define ALLOWED_TCP = { 22, 80, 443 }
table inet filter {
set blocked_ips {
type ipv4_addr
elements = { 203.0.113.100, 198.51.100.50 }
}
chain input {
type filter hook input priority 0; policy drop;
# Connection tracking
ct state established,related accept
ct state invalid drop
# Loopback
iif lo accept
# Drop blocked IPs
ip saddr @blocked_ips drop
# ICMP
ip protocol icmp limit rate 10/second accept
ip6 nexthdr ipv6-icmp limit rate 10/second accept
# Allowed services with rate limiting on SSH
tcp dport 22 ct state new limit rate 5/minute burst 10 packets accept
tcp dport { 80, 443 } accept
# Final reject
reject with icmpx type port-unreachable
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
}
chain output {
type filter hook output priority 0; policy accept;
}
}
Save this to /etc/nftables.conf and load it:
sudo nft -f /etc/nftables.conf
Verify:
sudo nft list ruleset
nft Commands Reference
| Command | Description |
|---|---|
nft list ruleset | Display the complete ruleset |
nft list tables | List all tables |
nft list table inet filter | Show rules in a specific table |
nft list chains | List all chains |
nft list chain inet filter input | Show rules in a specific chain |
nft list sets | List all named sets |
nft add table inet <name> | Create a new table |
nft delete table inet <name> | Delete a table and all its contents |
nft flush table inet <name> | Remove all rules from a table |
nft add chain inet <table> <chain> { type filter hook input priority 0 \; } | Create a base chain |
nft add rule inet <table> <chain> <expression> <verdict> | Add a rule to the end of a chain |
nft insert rule inet <table> <chain> <expression> <verdict> | Insert a rule at the beginning of a chain |
nft delete rule inet <table> <chain> handle <n> | Delete a rule by its handle number |
nft add set inet <table> <name> { type <type> \; } | Create a named set |
nft add element inet <table> <name> { <elements> } | Add elements to a set |
nft flush ruleset | Remove all tables, chains, and rules |
nft -f <file> | Load a ruleset from a file |
nft monitor | Watch for ruleset changes in real time |
Troubleshooting
Rules Not Taking Effect
Verify the ruleset is loaded:
sudo nft list ruleset
Check that your chains are attached to the correct hooks and have the expected priority:
sudo nft list chains
Ensure there are no conflicting iptables rules:
sudo iptables -L -n
Locked Out of SSH
If you lose SSH access after applying rules:
- Access the server through a console (cloud provider web console, KVM, or physical access)
- Flush all rules:
sudo nft flush ruleset - Verify access is restored
- Rebuild your rules, ensuring SSH is allowed before setting a drop policy
Debugging Rule Matching
Add counters to rules to see which ones are matching traffic:
sudo nft add rule inet filter input tcp dport 22 counter accept
View counters:
sudo nft list chain inet filter input
The counter output shows packets and bytes matched:
tcp dport 22 counter packets 150 bytes 12000 accept
Rules Not Persisting After Reboot
Ensure the nftables service is enabled:
sudo systemctl enable nftables
Save the current ruleset:
sudo nft list ruleset | sudo tee /etc/nftables.conf
Verify the configuration file is syntactically valid:
sudo nft -c -f /etc/nftables.conf
Kernel Module Issues
If nft commands fail, verify the required kernel modules are loaded:
lsmod | grep nf_tables
Load them manually if needed:
sudo modprobe nf_tables
sudo modprobe nft_chain_nat
Summary
nftables is the definitive replacement for iptables on modern Linux systems. Its unified command interface, native set and map support, atomic rule replacement, and cleaner syntax make it a significant improvement over the legacy toolset. Whether you are setting up a simple host firewall or a complex NAT gateway with rate limiting and dynamic sets, nftables provides the tools to build efficient, maintainable firewall configurations.
Key takeaways:
- Use the
inetfamily to handle both IPv4 and IPv6 in a single table - Always allow established/related connections and loopback traffic first
- Leverage named sets for managing dynamic lists of IPs and ports without rewriting rules
- Use rate limiting on authentication services like SSH to mitigate brute-force attacks
- Manage your ruleset in a file and load it atomically with
nft -f - Enable the nftables systemd service for persistence across reboots
- Use
iptables-translateandiptables-restore-translateto migrate existing rulesets
For a higher-level firewall management experience on Ubuntu, see our guide on Configuring UFW Firewall on Ubuntu Server. To complement your firewall with secure remote access, refer to SSH Hardening for Linux Servers.