Back to Linux guides

UFW vs nftables

Stanley Ulili
Updated on December 1, 2025

The Linux firewall landscape is shifting as nftables replaces iptables as the kernel's default packet filtering framework. This transition raises questions about where UFW fits, since it was built to simplify iptables management.

UFW (Uncomplicated Firewall) has spent over a decade making firewall management accessible to regular users. It wraps complex netfilter operations in commands that actually make sense to humans. Most Ubuntu and Debian users rely on it daily without thinking about what's happening underneath.

Nftables represents the future of Linux packet filtering. Developed by the same team that created iptables, it fixes long-standing performance issues and architectural limitations. But it's a low-level tool that talks directly to the kernel, which makes it powerful and intimidating in equal measure.

This guide explores both approaches, showing you what they're good at and where they struggle, so you can figure out which one makes sense for your servers.

What UFW brings to firewall management

UFW started as Ubuntu's answer to a persistent problem: servers were getting compromised because people found iptables too confusing to configure properly. Rather than teach everyone complex netfilter syntax, Ubuntu created a tool that handles common security tasks through simple commands.

The design philosophy is ruthlessly practical. Most servers need to open some ports and block others. They need to allow specific IP addresses and deny suspicious ones. UFW focuses on making these operations straightforward:

 
sudo ufw allow 80
 
sudo ufw deny from 203.0.113.50

Under the hood, UFW translates these commands into proper netfilter rules using whatever backend your system provides. On older systems, that's iptables. On newer systems with the nftables backend enabled, UFW generates nftables rules instead. You don't need to know or care which one your system uses.

UFW comes pre-installed on Ubuntu and ships with many Debian-based distributions. It sits quietly until you enable it, which means your server starts without firewall protection but also without accidentally blocking critical services during setup. The tool automatically handles IPv4 and IPv6, creating matching rules for both protocols without requiring separate commands.

Application profiles extend UFW's simplicity by letting you reference services by name instead of memorizing port numbers. Instead of remembering that SSH uses port 22, you just allow OpenSSH and UFW handles the details.

With UFW's practical approach established, you can see how nftables takes a completely different path by giving you direct control over kernel packet filtering.

Understanding nftables architecture

Nftables emerged from the Netfilter project as a complete redesign of how Linux handles packet filtering. The developers kept what worked about iptables while fixing fundamental limitations that had accumulated over two decades.

The architecture uses a single kernel interface instead of separate tools for IPv4 (iptables), IPv6 (ip6tables), ARP (arptables), and bridging (ebtables). One tool handles everything, which eliminates the confusion and duplication that plagued the old system.

The rule syntax in nftables looks more like a programming language than traditional firewall commands. You define tables that contain chains, and chains contain rules. Each table belongs to a family (ip, ip6, inet, arp, bridge, or netdev), which determines what kind of traffic it processes:

 
sudo nft add table inet filter
 
sudo nft add chain inet filter input { type filter hook input priority 0 \; }
 
sudo nft add rule inet filter input tcp dport 22 accept

This creates a table called "filter" in the "inet" family (which handles both IPv4 and IPv6), adds an input chain to that table, and inserts a rule accepting SSH traffic. The syntax is verbose and requires understanding the relationship between tables, chains, and rules.

Nftables performs significantly better than iptables when you have thousands of rules. It uses a different internal data structure that makes rule lookups faster, and it can update rules atomically without the brief disruptions that iptables sometimes causes.

The tool also introduces sets and maps, which let you group related items and create more efficient rules. Instead of writing 100 separate rules to block 100 IP addresses, you create a set containing those addresses and write one rule that references the set.

Having seen both approaches, comparing their characteristics side by side reveals where each tool excels and where it struggles.

Comparing capabilities and approach

These tools operate at different levels of abstraction, which shapes everything about how you use them:

Aspect UFW nftables
Abstraction level High-level wrapper Direct kernel interface
Primary users System administrators Network engineers, advanced users
Learning time Hours Weeks to months
Command verbosity Short, readable Long, technical
Configuration files Simple text format Structured rulesets
IPv4/IPv6 handling Automatic dual-stack Single "inet" family
Rule organization Flat structure Tables, chains, hierarchies
Performance Depends on backend Optimized for speed
Sets and maps Not supported Native support
Rule atomicity Sequential updates Atomic ruleset replacement
Syntax consistency Consistent interface Unified across all families
Backend flexibility Can use iptables or nftables Native kernel framework
Application profiles Built-in convenience Must specify manually
Default policies Simple deny/allow Configurable per chain
NAT configuration Basic support Advanced capabilities
Rule priorities Implicit ordering Explicit priority values

Understanding these differences conceptually helps, but seeing how you actually create rules with each tool makes the contrast much clearer.

Creating basic rules with UFW

UFW focuses on the most common firewall operations and makes them as simple as possible.

Opening a port requires just the port number:

 
sudo ufw allow 443

UFW automatically creates rules for both TCP and UDP on both IPv4 and IPv6. If you want only TCP:

 
sudo ufw allow 443/tcp

Blocking traffic from an IP address:

 
sudo ufw deny from 203.0.113.50

Allowing access from a specific network to a specific port:

 
sudo ufw allow from 192.168.1.0/24 to any port 22

The syntax reads naturally - allow connections from this network to port 22 on any interface.

Using application profiles instead of port numbers:

 
sudo ufw allow 'Nginx Full'

This opens both HTTP (80) and HTTPS (443) based on the application profile definition.

Checking your current rules:

 
sudo ufw status verbose
Output
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
443/tcp                    ALLOW IN    Anywhere
22                         ALLOW IN    192.168.1.0/24
Nginx Full                 ALLOW IN    Anywhere
443/tcp (v6)               ALLOW IN    Anywhere (v6)
Nginx Full (v6)            ALLOW IN    Anywhere (v6)

The output shows your rules in a readable table format that's easy to scan and understand.

Removing rules works just like adding them:

 
sudo ufw delete allow 443/tcp

Or you can reference rules by number:

 
sudo ufw status numbered
 
sudo ufw delete 2

The commands are short and predictable, which makes UFW fast for common tasks. But this simplicity reaches its limits when you need something unusual.

Having seen UFW's streamlined approach, exploring nftables reveals both the power and complexity of working directly with the kernel's packet filtering system.

Building firewall rules with nftables

Nftables requires understanding its structure before you can create even basic rules. You work with tables, chains, and rules as distinct concepts.

Creating a table for filtering:

 
sudo nft add table inet filter

The "inet" family handles both IPv4 and IPv6 traffic in a single table, which is cleaner than maintaining separate iptables and ip6tables rules.

Adding a chain to process incoming packets:

 
sudo nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }

This creates an input chain with a default drop policy. The priority value determines the order if multiple chains exist, and you need to escape the semicolons in most shells.

Adding a rule to allow SSH:

 
sudo nft add rule inet filter input tcp dport 22 accept

Allowing traffic from a specific network:

 
sudo nft add rule inet filter input ip saddr 192.168.1.0/24 tcp dport 22 accept

The "ip saddr" specifies IPv4 source address. For IPv6, you'd use "ip6 saddr".

Viewing your configuration:

 
sudo nft list ruleset
Output
table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;
    tcp dport 22 accept
    ip saddr 192.168.1.0/24 tcp dport 22 accept
  }
}

The output shows the complete ruleset structure, making the relationships between tables, chains, and rules explicit.

Deleting rules requires understanding handles. Each rule gets a unique handle that you must specify:

 
sudo nft --handle list ruleset
Output
table inet filter { # handle 1
  chain input { # handle 1
    type filter hook input priority 0; policy drop;
    tcp dport 22 accept # handle 3
    ip saddr 192.168.1.0/24 tcp dport 22 accept # handle 4
  }
}
 
sudo nft delete rule inet filter input handle 4

The handle system prevents accidentally deleting the wrong rule but adds extra steps to rule management.

Creating an entire ruleset in one operation demonstrates nftables' atomic updates:

firewall.nft
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;

    # Allow established connections
    ct state established,related accept

    # Allow loopback
    iif lo accept

    # Allow SSH from management network
    ip saddr 192.168.1.0/24 tcp dport 22 accept

    # Allow web traffic
    tcp dport { 80, 443 } accept
  }

  chain forward {
    type filter hook forward priority 0; policy drop;
  }

  chain output {
    type filter hook output priority 0; policy accept;
  }
}

Loading this ruleset:

 
sudo nft -f firewall.nft

This replaces your entire firewall configuration atomically, which prevents the brief gaps that can occur when updating rules one at a time. You've now configured basic filtering with nftables, but the real power emerges with advanced features.

Advanced features and performance

The tools diverge sharply when you move beyond basic port filtering.

UFW handles simple scenarios well but requires dropping to iptables or nftables for advanced features. Port forwarding, for example, needs editing configuration files:

/etc/ufw/before.rules
*nat
:PREROUTING ACCEPT [0:0]

# Forward port 80 to internal server
-A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 192.168.1.100:80

COMMIT

You're writing raw iptables rules, which defeats the purpose of using UFW.

Rate limiting in UFW uses a simple syntax:

 
sudo ufw limit 22/tcp

This implements basic protection against brute-force attacks, limiting connections from the same IP address.

Nftables includes advanced features as native functionality. Port forwarding uses DNAT in the prerouting chain:

 
sudo nft add table ip nat
 
sudo nft add chain ip nat prerouting { type nat hook prerouting priority -100 \; }
 
sudo nft add rule ip nat prerouting iif eth0 tcp dport 80 dnat to 192.168.1.100:80

Sets make managing lists of addresses or ports efficient. Creating a set of blocked IPs:

 
sudo nft add set inet filter blocked_ips { type ipv4_addr \; flags interval \; }

Adding addresses to the set:

 
sudo nft add element inet filter blocked_ips { 203.0.113.50, 203.0.113.100-203.0.113.200 }

Using the set in a rule:

 
sudo nft add rule inet filter input ip saddr @blocked_ips drop

One rule now blocks all addresses in the set, and you can add or remove addresses without modifying the rule itself.

Maps create associations between values. This example redirects different ports to different servers:

 
sudo nft add map inet filter port_forwards { type inet_service : ipv4_addr \; }
 
sudo nft add element inet filter port_forwards { 8001 : 192.168.1.101, 8002 : 192.168.1.102, 8003 : 192.168.1.103 }
 
sudo nft add rule inet nat prerouting dnat to tcp dport map @port_forwards

Performance matters when you're processing millions of packets. Nftables uses a bytecode-based virtual machine that evaluates rules much faster than iptables' linear rule traversal. In tests with thousands of rules, nftables often performs 10-100 times faster.

The atomic ruleset replacement in nftables also eliminates race conditions. With iptables, updating rules happens sequentially, creating brief moments where partial configurations exist. Nftables loads complete rulesets in one operation, ensuring consistency.

These capabilities show nftables' power, but they come with significant complexity that affects daily operations.

Configuration management and persistence

Saving and restoring firewall rules works very differently between these tools.

UFW automatically persists every change you make. Run a command, and UFW writes it to configuration files in /etc/ufw/. On boot, UFW reads these files and recreates your rules. You never think about saving or loading.

Viewing UFW's generated rules:

 
sudo cat /etc/ufw/user.rules
Output
*filter
:ufw-user-input - [0:0]

### RULES ###

### tuple ### allow tcp 22 0.0.0.0/0 any 0.0.0.0/0 in
-A ufw-user-input -p tcp --dport 22 -j ACCEPT

### tuple ### allow tcp 80 0.0.0.0/0 any 0.0.0.0/0 in
-A ufw-user-input -p tcp --dport 80 -j ACCEPT

### END RULES ###
COMMIT

This shows the actual iptables rules UFW generates, which helps when you need to understand or debug what's happening.

Backing up UFW configuration:

 
sudo tar -czf ufw-backup-$(date +%Y%m%d).tar.gz /etc/ufw

Restoring involves extracting the archive and reloading UFW:

 
sudo tar -xzf ufw-backup-20250115.tar.gz -C /
 
sudo ufw reload

Nftables requires explicit saving and loading of rulesets. Your working ruleset exists only in memory until you save it:

 
sudo nft list ruleset > /etc/nftables.conf

Many distributions configure nftables to load /etc/nftables.conf at boot, but you need to verify this is set up correctly:

 
sudo systemctl enable nftables
 
sudo systemctl status nftables

Loading a saved ruleset:

 
sudo nft -f /etc/nftables.conf

The explicit save step gives you more control but adds responsibility. Forget to save after making changes, and they disappear on reboot.

Atomic ruleset replacement in nftables makes testing safer. You can load a test ruleset, verify it works, then save it permanently:

 
sudo nft -f test-firewall.nft

Test your applications and connections. If something breaks:

 
sudo nft -f /etc/nftables.conf

This instantly reverts to your saved configuration without the incremental rollback required with UFW.

Version controlling your firewall configuration works better with nftables because the ruleset format is designed for files:

 
sudo git init /etc/nftables
 
sudo cp /etc/nftables.conf /etc/nftables/
 
cd /etc/nftables && sudo git add nftables.conf && sudo git commit -m "Initial firewall config"

UFW's configuration spreads across multiple files with generated content mixed with your rules, making version control messier.

With persistence covered, considering how these tools integrate with your broader infrastructure helps determine which fits your workflow.

System integration and tooling

The way firewall tools connect with other system components has a big impact on your operational experience.

UFW operates independently of most system services. It doesn't integrate with NetworkManager, systemd in complex ways, or other infrastructure. This simplicity means fewer moving parts but also fewer opportunities for automation.

Logging in UFW goes to /var/log/ufw.log:

 
sudo tail -f /var/log/ufw.log
Output
Nov 27 15:42:11 server kernel: [UFW BLOCK] IN=eth0 OUT= MAC=00:00:00:00:00:00 SRC=203.0.113.42 DST=192.168.1.10 LEN=60 TOS=0x00 PREC=0x00 TTL=54 ID=12345 PROTO=TCP SPT=45678 DPT=22

The log format is readable but separate from systemd's journal, which means you need to check multiple places when troubleshooting system issues.

UFW doesn't provide built-in support for dynamic rule updates based on external triggers. If you want to automatically block IPs that fail authentication, you need scripts that parse logs and call UFW commands:

block-failed-ssh.sh
#!/bin/bash

# Parse auth.log for failed SSH attempts
failed_ips=$(grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort -u)

for ip in $failed_ips; do
  sudo ufw deny from $ip
done

This works but requires custom scripting and coordination.

Nftables integrates more deeply with modern Linux systems. Logging connects directly to the kernel's netlink interface and can output to syslog or nftrace:

 
sudo nft add rule inet filter input tcp dport 22 log prefix "SSH-Access: " accept
 
sudo journalctl -k | grep SSH-Access
Output
Nov 27 15:45:32 server kernel: SSH-Access: IN=eth0 OUT= MAC=00:00:00:00:00:00 SRC=192.168.1.50 DST=192.168.1.10 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=54321 DF PROTO=TCP SPT=52431 DPT=22

The integration with journald means your firewall logs appear alongside other system events, making correlation easier.

Nftables supports ruleset includes, which enables modular firewall configurations:

/etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

include "/etc/nftables.d/defines.nft"
include "/etc/nftables.d/filter.nft"
include "/etc/nftables.d/nat.nft"

Each file can be managed separately, edited by different team members, or generated by different tools. This modularity scales better for complex configurations.

Dynamic rule updates work smoothly with nftables' set functionality. An intrusion detection system can add IPs to a blocklist set without reloading the entire ruleset:

 
sudo nft add element inet filter blocked_ips { 203.0.113.75 }

This takes effect immediately and doesn't interrupt existing connections.

Many configuration management tools have better nftables integration because it's the future of Linux packet filtering. Ansible's nftables module handles complex rulesets:

nftables-playbook.yml
---
- name: Configure nftables
  hosts: webservers
  become: yes
  tasks:
    - name: Deploy nftables configuration
      nftables:
        state: present
        table: inet filter
        chain: input
        rule: |
          tcp dport { 80, 443 } accept

UFW has Ansible support too, but it's limited to the operations UFW itself supports, which excludes many advanced features.

Understanding integration matters, but the practical question is how long it takes to become productive with each tool.

Learning curve and complexity

The time investment required to use these tools effectively differs dramatically.

UFW's learning curve is gentle. The commands map directly to what you want to accomplish:

Want to allow HTTP? sudo ufw allow 80 Want to block an IP? sudo ufw deny from 203.0.113.50 Want to see what's configured? sudo ufw status

You can secure a basic server within 30 minutes of first encountering UFW. The syntax is forgiving enough that you can often guess correct commands without consulting documentation.

The challenge with UFW comes when you need something it doesn't directly support. At that point, you're either stuck or you're learning iptables/nftables anyway to write custom rules. This limitation means UFW has a low entry point but a hard ceiling.

Nftables requires understanding packet filtering concepts before you can do anything. You need to know:

  • What tables and chains are
  • How hooks work (prerouting, input, forward, output, postrouting)
  • The difference between filter, nat, and route table types
  • How priority values affect chain ordering
  • When to use different address families

This conceptual foundation takes time to build. Even creating a basic firewall requires several commands and understanding their relationships:

 
sudo nft add table inet filter
 
sudo nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }
 
sudo nft add rule inet filter input ct state established,related accept
 
sudo nft add rule inet filter input tcp dport 22 accept

Each command makes sense once you understand the model, but the model itself requires study. Most people need several days of experimentation before feeling comfortable with nftables.

The payoff for this investment is flexibility. Once you understand nftables, you can implement virtually any packet filtering scenario. UFW's simplicity eventually becomes limiting; nftables' complexity eventually becomes empowering.

Documentation reflects this difference. UFW's man page is concise and example-driven. Nftables documentation is extensive and assumes networking knowledge, with detailed sections on protocol headers, connection tracking, and kernel internals.

Community resources follow similar patterns. Questions about UFW usually get simple answers. Questions about nftables often lead to discussions about the proper architecture for the solution, not just the specific syntax.

With learning curves clear, you can assess which tool matches your actual needs and capabilities.

Final thoughts

At a high level, UFW and nftables live at different layers of the same stack: UFW gives you a simple way to express intent, while nftables gives you full, low-level control.

If your goal is to secure a few servers quickly and keep the mental overhead low, UFW is usually the right starting point, especially on Ubuntu or Debian. By contrast, if you are designing complex networks, pushing high traffic, or need advanced features and tight performance, it makes sense to invest the time to learn nftables.

Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.