How the Three Tools Relate to the Kernel

iptables, nftables, and ufw are not competing implementations of the same thing. They sit at different layers.

nftables is the kernel-level packet classification framework that replaced the older xtables infrastructure starting in Linux 3.13. As of kernel 5.2, nftables has full feature parity with iptables and is the preferred interface to netfilter. iptables in modern distros is typically `iptables-nft`, a compatibility shim that translates iptables syntax into nftables rules. You can verify which backend your system uses:

`update-alternatives --display iptables`

On RHEL 9 and Ubuntu 22.04+, this will show `/usr/sbin/iptables-nft` as the current selection. On older CentOS 7 systems, you will see the legacy `/usr/sbin/iptables-legacy` path.

ufw (Uncomplicated Firewall) is an abstraction layer that generates either iptables or nftables rules depending on the distro version. It is a frontend, not a framework. It reads profiles from `/etc/ufw/applications.d/`, manages default policies, and writes the actual rules to files that get loaded at boot. It does not give you direct access to the rule table.

update-alternatives --display iptables
iptables --version
nft --version

iptables: Still Valid, But Showing Its Age

iptables has been the default Linux firewall interface since the late 1990s. Its strength is familiarity: virtually every sysadmin knows the syntax, every tutorial covers it, and every monitoring tool understands it. Its weakness is structural: the rule evaluation model is linear and each table (filter, nat, mangle, raw) is separate, which means complex rulesets become slow and unreadable.

Rule lookup in iptables is O(n) per packet. On a busy router with 500+ rules, this is measurable. We tested on a 10Gbps interface with 450 iptables ACCEPT/DROP rules and saw a 4-6% throughput reduction versus a clean nftables ruleset doing identical work. That gap widens with rule count.

The syntax is also verbose. A basic stateful firewall allowing established connections and SSH looks like this:

``` iptables -P INPUT DROP iptables -P FORWARD DROP iptables -P OUTPUT ACCEPT iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -A INPUT -i lo -j ACCEPT iptables -A INPUT -p tcp --dport 22 -j ACCEPT ```

To persist these across reboots on Debian-based systems you need `iptables-persistent`. On RHEL you use `iptables.service` with `iptables-save > /etc/sysconfig/iptables`. Neither approach is atomic - there is a window between boot and rule application where the default policy applies.

For IPv6 you need a completely separate invocation of `ip6tables` with duplicated rules. This is the single biggest operational annoyance with iptables in 2026. Any ruleset managing dual-stack traffic doubles in maintenance surface.

iptables -P INPUT DROP
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6

nftables: The Right Answer for New Infrastructure

nftables ships as the default in RHEL 8+, Debian 10+, and Ubuntu 20.04+. Its ruleset language is a single unified syntax for IPv4, IPv6, ARP, and bridge filtering. One table handles everything.

The equivalent stateful SSH ruleset in nftables:

``` table inet filter { chain input { type filter hook input priority 0; policy drop; ct state established,related accept iif lo accept tcp dport 22 accept } chain forward { type filter hook forward priority 0; policy drop; } chain output { type filter hook output priority 0; policy accept; } } ```

Load this with `nft -f /etc/nftables.conf` and it applies atomically. The entire ruleset is replaced in a single kernel operation, eliminating the window vulnerability that iptables has during rule reloads.

nftables supports sets and maps natively, which changes how you handle large allow/deny lists. Instead of 200 individual rules for a blocklist:

``` table inet filter { set blocklist { type ipv4_addr flags interval elements = { 192.168.100.0/24, 10.0.0.5 } } chain input { type filter hook input priority 0; policy drop; ip saddr @blocklist drop ct state established,related accept tcp dport 22 accept } } ```

Set lookup is hash-based, so it is O(1) regardless of set size. We added 10,000 IP entries to a blocklist set and saw zero measurable throughput impact compared to an empty set. With iptables, 10,000 individual DROP rules reduced throughput by over 30% on the same hardware.

The nftables service on systemd systems is straightforward:

`systemctl enable --now nftables`

Rulesets are validated before being committed. If your config file has a syntax error, `nft -f /etc/nftables.conf` fails without touching the running rules. This makes automated deployments safer. If you are using tools like taskbotshub.ai for infrastructure automation pipelines, nftables' atomic reload and file-based configuration fits cleanly into idempotent apply workflows without the state-drift problems that iptables direct commands introduce.

nft -f /etc/nftables.conf
nft list ruleset
nft list table inet filter
nft flush ruleset
// advertisement

ufw: The Right Tool for the Right Job

ufw exists for a specific use case: servers where you want a quick, maintainable firewall without learning netfilter internals. It is the correct choice for a VPS running a web application where your firewall requirements are 'allow 80, 443, 22, block everything else'.

The complete setup for that case:

``` ufw default deny incoming ufw default allow outgoing ufw allow ssh ufw allow 'Nginx Full' ufw enable ```

Application profiles in `/etc/ufw/applications.d/` define named port sets. This is genuinely useful for humans reading audit logs or handing off server management.

Where ufw breaks down is complexity. Rate limiting is available but limited:

`ufw limit ssh`

This applies a basic connlimit rule (6 connections per 30 seconds), but you cannot customize the parameters without editing the underlying generated files directly, which then get overwritten on the next `ufw reload`. Logging is binary: on or off, with no per-rule control. There is no native support for sets, maps, or connmark-based routing.

ufw also has a specific problem in 2026: the generated rules it writes vary by distro version in ways that are not obvious. On Ubuntu 24.04, `ufw` writes nftables rules by default. On Ubuntu 20.04, it still writes iptables-legacy rules. If you are managing a mixed fleet, running `ufw status verbose` on two nominally identical servers can show rule differences that are invisible at the ufw layer.

Check what ufw is actually generating:

`nft list ruleset` or `iptables-save` depending on your backend.

For a single-tenant VPS or development server, ufw is the lowest friction option. For any production environment with more than trivial requirements, you will eventually need to bypass ufw and touch the underlying nftables config directly, at which point you should question why you are running two layers.

ufw status verbose
ufw show added
ufw show listening
nft list ruleset | grep -A5 'ufw'

Performance Benchmarks: What the Numbers Show

We ran packet throughput tests on a bare-metal server with a 10Gbps NIC, Ubuntu 24.04, kernel 6.8, using `pktgen` to generate traffic and measuring with `sar -n DEV 1`.

Test conditions: 500 ACCEPT rules followed by a DROP all, with traffic matching the last ACCEPT rule (worst case for linear scanning).

Results: - iptables-legacy with 500 rules: 6.1 Gbps throughput - iptables-nft shim with 500 rules: 5.8 Gbps throughput (shim overhead is real) - nftables with 500 rules in a chain: 5.9 Gbps throughput - nftables with same 500 rules collapsed into a set: 9.4 Gbps throughput

The set result is the important one. If your firewall needs to permit or block based on IP address lists, the difference between nftables sets and iptables individual rules is not marginal - it is the difference between saturating your link or not.

For connection tracking overhead, all three tools use the same kernel conntrack subsystem, so there is no difference there. The overhead is in rule evaluation, not connection state management.

One note on the iptables-nft shim: the translation layer adds latency that is invisible in low-throughput scenarios but shows up under load. If you have legacy automation or tooling that generates iptables commands, the shim keeps things working, but you are paying a price. Migrating to native nftables syntax is worth the effort on high-traffic systems.

# Check active conntrack table size
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

# Dump nftables rule counts
nft list ruleset | grep -c 'accept\|drop\|reject'

Migration Path: Moving from iptables to nftables

The `iptables-translate` and `iptables-restore-translate` tools convert existing iptables rules to nftables syntax. They ship with the `iptables` package on modern distros.

``` iptables-save > /tmp/iptables-rules.txt iptables-restore-translate -f /tmp/iptables-rules.txt > /etc/nftables-migrated.conf nft -c -f /etc/nftables-migrated.conf ```

The `-c` flag validates without applying. Review the output before loading. The translation is mechanical and will produce working rules, but it will not consolidate separate IPv4/IPv6 rules into unified inet tables, and it will not replace lists of individual IP rules with sets. You will want to refactor the output rather than use it verbatim in production.

For environments where multiple teams or tools generate firewall rules (Kubernetes CNI plugins, Docker, custom scripts), the migration is more complex. Docker's iptables integration is a known source of conflicts. When Docker starts, it inserts its own DOCKER and DOCKER-USER chains. With nftables as the primary firewall, you need to disable Docker's iptables management (`--iptables=false` in `/etc/docker/daemon.json`) and manage container networking rules manually in nftables, or use a CNI plugin that has native nftables support.

Check for active Docker iptables rules before migrating:

`iptables -L DOCKER -n --line-numbers`

If you are running Kubernetes, verify your CNI plugin's nftables support status. Calico has had experimental nftables support since v3.27. Cilium uses eBPF and bypasses iptables and nftables entirely for its datapath, making it the cleanest option if you are rebuilding your network stack anyway.

iptables-save > /tmp/old-rules.txt
iptables-restore-translate -f /tmp/old-rules.txt > /etc/nftables-migrated.conf
nft -c -f /etc/nftables-migrated.conf
diff <(nft list ruleset) /etc/nftables-migrated.conf
// advertisement

Logging and Auditing Across All Three

Logging behavior differs enough between the three tools to affect your audit posture.

With iptables, you add LOG targets before final rules:

`iptables -A INPUT -p tcp --dport 22 -j LOG --log-prefix "SSH-ACCEPT: " --log-level 4`

With nftables, logging is a statement, not a target chain jump:

``` table inet filter { chain input { tcp dport 22 log prefix "SSH-ACCEPT: " level info accept } } ```

This is cleaner because logging and the accept/drop action are combined in one rule. You can also log to named groups for nfnetlink-based log consumers:

`tcp dport 22 log group 0 accept`

With ufw, you enable logging globally:

`ufw logging on` or `ufw logging medium`

The medium level logs blocked packets and new connections matching allow rules. You cannot log specific rules without editing the generated iptables/nftables files directly.

All three tools ultimately write to the kernel log buffer, readable via `journalctl -k -f` or `/var/log/kern.log`. For structured log shipping, nftables' nfnetlink group logging pairs cleanly with `ulogd2` to get JSON-formatted firewall events without parsing kernel message strings.

For compliance environments needing per-rule audit trails, nftables is the only option that gives you fine-grained control over what gets logged and at what level without workarounds.

# Watch nftables log output in real time
journalctl -k -f | grep --line-buffered 'nft\|SSH-ACCEPT'

# List nftables log rules only
nft list ruleset | grep log