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 nft command 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 inet family handles both IPv4 and IPv6 in a single table

nftables vs iptables

Understanding the key differences helps when transitioning:

Featureiptablesnftables
Commandiptables, ip6tables, arptables, ebtablesnft (single tool)
Address familiesSeparate tool per familyUnified inet family for IPv4/IPv6
SyntaxFlag-based (-A, -j, -p)Structured grammar (add rule ... accept)
SetsRequires ipset extensionNative sets and maps built-in
Rule updatesLinear rule insertion/deletionAtomic ruleset replacement via -f
Pre-built chainsMandatory built-in chains (INPUT, OUTPUT, FORWARD)No defaults; you create tables and chains as needed
PerformanceLinear rule evaluationOptimized set lookups, binary expressions
Kernel interfacextables (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:

FamilyDescription
ipIPv4 traffic only
ip6IPv6 traffic only
inetBoth IPv4 and IPv6 (recommended)
arpARP protocol
bridgeBridge-level traffic
netdevIngress 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

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:

StateDescription
newFirst packet of a new connection
establishedPart of an already established connection
relatedRelated to an established connection (e.g., FTP data channel)
invalidNot associated with any known connection
untrackedExplicitly 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

  1. Export existing rules — save your current iptables rules before starting
  2. Translate and review — use iptables-restore-translate and carefully review the output
  3. Test in a non-production environment — apply the translated rules on a test server first
  4. Flush old rules — once nft rules are confirmed, flush iptables rules with iptables -F
  5. Disable iptables service — stop and disable the iptables service to avoid conflicts
  6. 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

CommandDescription
nft list rulesetDisplay the complete ruleset
nft list tablesList all tables
nft list table inet filterShow rules in a specific table
nft list chainsList all chains
nft list chain inet filter inputShow rules in a specific chain
nft list setsList 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 rulesetRemove all tables, chains, and rules
nft -f <file>Load a ruleset from a file
nft monitorWatch 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:

  1. Access the server through a console (cloud provider web console, KVM, or physical access)
  2. Flush all rules: sudo nft flush ruleset
  3. Verify access is restored
  4. 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 inet family 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-translate and iptables-restore-translate to 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.