Iptables vs UFW: Which Linux Firewall Should You Use?
Securing a Linux server means controlling network traffic, and that's where firewalls come in. But Linux gives you choices, and the two main options - iptables and UFW - take completely different approaches to the same job.
Iptables is the low-level tool that's been around forever. It talks directly to the Linux kernel's netfilter framework and gives you complete control over every packet that touches your network interfaces. But with that power comes complexity - iptables commands are dense, cryptic, and easy to mess up.
UFW was created specifically to make firewall management less painful. It wraps iptables in a friendlier interface, letting you accomplish common security tasks without needing to understand kernel networking internals.
This guide walks through both tools, showing you what they're good at, where they fall short, and helping you figure out which one fits your needs.
How iptables works
Iptables connects directly to Linux's netfilter system, which lives in the kernel and makes decisions about every network packet. When a packet arrives, netfilter checks it against your rules and decides whether to accept it, drop it, or do something else with it.
The power of iptables comes from this direct connection. You can write rules that match packets based on source address, destination port, protocol type, packet state, and dozens of other criteria. You can modify packets, redirect them, log them, or count them. If netfilter can do it, iptables can configure it.
But this power has a price. An iptables rule looks like this:
sudo iptables -A INPUT -p tcp -s 192.168.1.100 --dport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
That single line opens SSH access from one IP address. It includes the chain name (INPUT), protocol specification (-p tcp), source address (-s), destination port (--dport), connection state matching (-m conntrack --ctstate), and the action to take (-j ACCEPT). Miss any piece and the rule won't work as expected.
Iptables organizes rules into tables and chains. The most common table is the filter table, which contains three chains: INPUT (for incoming packets), OUTPUT (for outgoing packets), and FORWARD (for packets being routed through the system). Rules get evaluated in order, and the first match wins.
What UFW brings to Linux
UFW started as an Ubuntu project to make firewall management accessible to regular users. The goal was simple: give people a way to secure their systems without needing a networking degree.
Instead of long command strings with multiple flags, UFW uses straightforward English-like syntax:
sudo ufw allow from 192.168.1.100 to any port 22
That accomplishes the same thing as the iptables command above, but you can actually read it and understand what it does. The command structure makes sense - allow traffic from this address to this port.
UFW doesn't replace iptables. It sits on top of it, translating your simple commands into the complex iptables rules that netfilter actually uses. When you enable UFW and add rules, it's writing iptables rules behind the scenes. You can verify this by checking iptables directly after making UFW changes.
The trade-off is that UFW can't do everything iptables can do. It focuses on the common use cases - opening and closing ports, allowing or denying specific addresses, setting up basic NAT. For most servers, that's plenty.
Comparing the two approaches
Looking at these tools side by side shows you what you gain and lose with each approach:
| Aspect | iptables | UFW |
|---|---|---|
| Command complexity | Very complex, many flags | Simple, readable commands |
| Learning time | Weeks to months | Hours to days |
| Default availability | On every Linux system | Ubuntu/Debian by default |
| Rule capabilities | Complete packet control | Common scenarios covered |
| Configuration persistence | Requires save mechanism | Automatic on rule changes |
| Application profiles | None built-in | Includes common apps |
| Rule priority control | Complete control via order | Limited explicit priority |
| IPv6 support | Separate ip6tables command | Handles both automatically |
| NAT configuration | Full control, complex setup | Basic NAT, simpler syntax |
| Logging options | Extensive, granular control | Basic logging levels |
| Integration with kernel | Direct netfilter interface | Abstraction layer |
| Advanced matching | All netfilter modules | Subset of common options |
| Performance overhead | None | Minimal translation layer |
| Documentation depth | Extensive but technical | User-friendly guides |
| Community solutions | Decades of examples | Growing collection |
Writing iptables rules
Creating iptables rules means understanding chains, tables, and how packets flow through the system. Each rule specifies criteria for matching packets and an action to take when packets match.
A basic rule to allow web traffic looks like this:
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
Breaking this down: -A INPUT appends the rule to the INPUT chain, -p tcp matches TCP protocol packets, --dport 80 specifies destination port 80, and -j ACCEPT jumps to the ACCEPT target (allows the packet through).
To see your current rules:
sudo iptables -L -v -n
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
156 12480 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
The -L flag lists rules, -v adds verbose output showing packet counts, and -n displays addresses numerically instead of trying to resolve hostnames.
Blocking an IP address requires specifying the source:
sudo iptables -A INPUT -s 203.0.113.50 -j DROP
The -j DROP target silently discards matching packets without sending any response.
Connection state tracking adds another layer. This rule allows established connections to continue:
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
The -m conntrack loads the connection tracking module, and --ctstate specifies which connection states to match. This lets reply packets through without needing separate rules for each service.
Setting a default policy determines what happens to packets that don't match any rule:
sudo iptables -P INPUT DROP
The -P flag sets the policy for the INPUT chain to DROP, blocking everything not explicitly allowed. This "deny by default" approach is more secure but requires careful rule configuration.
Now that you've seen iptables' complexity, let's look at how UFW simplifies these same tasks.
Creating rules with UFW
UFW takes firewall management and makes it conversational. Instead of memorizing flags and kernel modules, you describe what you want in plain terms.
Opening a port is straightforward:
sudo ufw allow 80
That's it. UFW figures out you want both TCP and UDP on port 80, creates the necessary iptables rules, and handles IPv4 and IPv6 automatically.
If you only want TCP:
sudo ufw allow 80/tcp
The syntax remains clear - just add the protocol after a slash.
Restricting access by source address reads naturally:
sudo ufw allow from 192.168.1.0/24 to any port 22
You can read this command and understand it immediately. Allow connections from the 192.168.1.0/24 network to port 22 on any interface.
UFW includes application profiles for common services. Instead of remembering port numbers, you can reference the application:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
Application profiles live in /etc/ufw/applications.d/ and define the ports and protocols each application needs. You can create custom profiles for your own applications.
Checking your firewall status gives you a clear overview:
sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
80/tcp ALLOW IN Anywhere
22 ALLOW IN 192.168.1.0/24
Nginx Full ALLOW IN Anywhere
80/tcp (v6) ALLOW IN Anywhere (v6)
Nginx Full (v6) ALLOW IN Anywhere (v6)
The output shows you exactly what's allowed and from where, formatted in a way that's easy to scan.
Removing a rule mirrors adding it:
sudo ufw delete allow 80/tcp
Or you can remove by rule number:
sudo ufw status numbered
sudo ufw delete 3
UFW's simpler syntax makes these operations faster once you know what you want to do, but it hides details that sometimes matter.
Handling complex scenarios
Where UFW and iptables really diverge is in handling unusual or advanced configurations.
Iptables lets you match packets on practically anything. Need to block packets with specific TCP flags? You can do that:
sudo iptables -A INPUT -p tcp --tcp-flags ALL FIN,URG,PSH -j DROP
This drops packets with suspicious flag combinations that might indicate port scanning.
Want to rate-limit connections to prevent brute force attacks?
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --set
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 4 -j DROP
These rules track connection attempts and drop new connections from IPs that try more than 4 times in 60 seconds. The recent module maintains a list of source addresses and timestamps.
You can also do packet mangling - modifying packet headers as they pass through:
sudo iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TOS --set-tos 0x10
This sets the Type of Service field to minimize delay for web traffic.
UFW handles common rate limiting with a simpler syntax:
sudo ufw limit 22/tcp
This implements basic rate limiting on SSH connections using the same underlying iptables recent module, but you don't control the specific thresholds.
For more complex scenarios, UFW starts showing its limitations. You can't match on arbitrary packet fields or use many of iptables' advanced modules. When you hit these walls, you have two choices: add custom iptables rules alongside UFW, or switch to managing iptables directly.
With the technical differences clear, let's talk about what this means in practice.
Rule persistence and management
One significant difference between these tools is how they handle saving your configuration.
Iptables rules exist only in memory. When you add a rule, it takes effect immediately, but if you reboot your server, that rule disappears. To make rules persistent, you need to save them:
sudo iptables-save > /etc/iptables/rules.v4
Then configure your system to restore these rules at boot. Different distributions handle this differently. On Debian and Ubuntu, you can install the iptables-persistent package:
sudo apt install iptables-persistent
During installation, it asks if you want to save current rules. After that, it automatically restores rules at boot from files in /etc/iptables/.
Making changes to persistent rules requires editing the saved files or using iptables-restore:
sudo iptables-save | sudo tee /etc/iptables/rules.v4
UFW handles persistence automatically. Every rule you add gets written to configuration files in /etc/ufw/. When UFW starts at boot, it reads these files and recreates all your rules. You never think about saving or restoring.
This automatic persistence makes UFW more beginner-friendly, but it also means less control over exactly when and how rules get applied.
Rule organization differs too. With iptables, you see every single rule in the order they'll be evaluated:
sudo iptables -L INPUT --line-numbers
Chain INPUT (policy DROP)
num target prot opt source destination
1 ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
2 ACCEPT tcp -- anywhere anywhere tcp dpt:ssh
3 ACCEPT tcp -- anywhere anywhere tcp dpt:http
4 ACCEPT tcp -- anywhere anywhere tcp dpt:https
UFW abstracts this into a simpler view but gives you less visibility into rule order and priority.
Understanding persistence is important, but day-to-day operations matter more.
Working with each tool daily
The way these tools fit into your regular workflow has a huge impact on how reliably you'll use them.
Iptables commands are verbose, so most people create shell scripts or use configuration management tools. A typical deployment might look like:
#!/bin/bash
# Flush existing rules
iptables -F
iptables -X
# Default policies
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
# Allow established connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow SSH
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Allow HTTP and HTTPS
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Save rules
iptables-save > /etc/iptables/rules.v4
You run this script to configure your firewall, and it's repeatable and version-controllable. But creating and maintaining the script requires understanding all those iptables flags.
UFW encourages interactive management through simple commands. Most people don't script UFW because the commands are already simple enough to type:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable
You can script these, but it's less common because the individual commands are so quick to type.
Debugging differs significantly. With iptables, you can watch packets in real-time:
sudo iptables -A INPUT -j LOG --log-prefix "IPTABLES-DEBUG: "
sudo tail -f /var/log/kern.log | grep IPTABLES-DEBUG
This logs every packet hitting the INPUT chain, helping you see exactly what your firewall is doing.
UFW's logging is simpler:
sudo ufw logging on
sudo tail -f /var/log/ufw.log
You get less control over what gets logged and how, but it's easier to set up.
Both tools work fine over SSH, but iptables has one dangerous gotcha: if you set a default DROP policy without first allowing SSH, you lock yourself out immediately. UFW protects against this - when you enable it, it automatically creates rules for any services you're currently connected through.
These workflow differences influence which tool works better for different situations.
Combining both approaches
You're not forced to choose just one tool. Many administrators use UFW for day-to-day management but drop into iptables for specific advanced needs.
UFW stores its generated iptables rules in files you can examine:
sudo cat /etc/ufw/user.rules
*filter
:ufw-user-input - [0:0]
:ufw-user-output - [0:0]
:ufw-user-forward - [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
You can see exactly what iptables rules UFW created. And you can add custom rules in the before.rules or after.rules files:
# Custom rate limiting for specific service
-A ufw-before-input -p tcp --dport 8080 -m conntrack --ctstate NEW -m recent --set
-A ufw-before-input -p tcp --dport 8080 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 10 -j DROP
These custom rules get applied before UFW's generated rules, giving you iptables-level control where you need it while keeping UFW's simpler interface for everything else.
You can also verify UFW's work by checking iptables directly:
sudo iptables -L -n -v
This shows you all the chains UFW created and how traffic flows through them. It's useful for understanding what's actually happening and debugging issues.
The hybrid approach works well but requires understanding both tools. You need to know enough iptables to write custom rules correctly and enough UFW to understand how it organizes its chains.
Final thoughts
Iptables and UFW solve the same problem but are aimed at different kinds of users and different levels of complexity.
If you need fine-grained control, are working in high-security or specialized environments, or want to master how Linux networking really works, iptables is the better fit, even though it takes more time and effort to learn. On the other hand, if you mostly run standard servers, work with teams that have mixed experience, or prefer to focus on applications instead of packet rules, UFW usually gives you everything you need with much less friction.