Linux Firewall Configuration: iptables, nftables and firewalld Compared
The Linux kernel has a powerful packet filtering framework built in, but the userspace tools for managing it have evolved significantly over the years. Today, administrators face a choice between iptables (the legacy standard), nftables (its modern replacement), firewalld (a dynamic management layer), and UFW (a simplicity-focused frontend). Each has its strengths, and understanding the differences is critical for building effective firewall rules -- especially on Docker hosts where container networking adds significant complexity.
Architecture Overview
All Linux firewall tools ultimately interact with the same kernel framework. Understanding the architecture helps you choose the right tool:
| Tool | Backend | Default On | Strengths |
|---|---|---|---|
iptables |
xtables (netfilter) | Debian, Ubuntu (legacy) | Universal knowledge base, extensive documentation |
nftables |
nf_tables (netfilter) | Debian 10+, Arch, Fedora | Modern syntax, atomic rule replacement, better performance |
firewalld |
nftables or iptables | RHEL, CentOS, Fedora | Zone-based, dynamic rules, D-Bus API |
ufw |
iptables | Ubuntu | Simple syntax, beginner-friendly |
Note: On modern systems, the
iptablescommand often uses theiptables-nftbackend, which translates iptables syntax to nftables rules internally. Check withiptables --version-- if it saysnf_tables, you are already using nftables under the hood.
iptables: Chains and Rules
iptables organizes rules into tables and chains. The most commonly used table is filter, which contains three default chains:
- INPUT -- Packets destined for the local machine
- FORWARD -- Packets being routed through the machine
- OUTPUT -- Packets generated by the local machine
# View current rules
iptables -L -n -v # List rules with line numbers
iptables -L -n --line-numbers
# Set default policies
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# Allow established and related connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Drop invalid packets
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
# Allow SSH
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
# Allow HTTP/HTTPS
iptables -A INPUT -p tcp -m multiport --dports 80,443 \
-m conntrack --ctstate NEW -j ACCEPT
# Allow ICMP (ping)
iptables -A INPUT -p icmp --icmp-type echo-request \
-m limit --limit 5/sec -j ACCEPT
# Log dropped packets
iptables -A INPUT -m limit --limit 5/min \
-j LOG --log-prefix "iptables-drop: " --log-level 4
# Drop everything else (redundant with policy but explicit)
iptables -A INPUT -j DROP
Rate Limiting with iptables
# Limit SSH connections to 3 per minute per source IP
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m recent --set --name SSH --rsource
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m recent --update --seconds 60 --hitcount 4 --name SSH --rsource -j DROP
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
# Rate limit HTTP connections
iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW \
-m limit --limit 100/sec --limit-burst 200 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j DROP
Port Forwarding with iptables
# Enable IP forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward
# Forward external port 8080 to internal host 192.168.1.100:80
iptables -t nat -A PREROUTING -p tcp --dport 8080 \
-j DNAT --to-destination 192.168.1.100:80
iptables -t nat -A POSTROUTING -p tcp -d 192.168.1.100 --dport 80 \
-j MASQUERADE
iptables -A FORWARD -p tcp -d 192.168.1.100 --dport 80 -j ACCEPT
Saving and Restoring Rules
# Save current rules
iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6
# Restore rules (typically done at boot)
iptables-restore < /etc/iptables/rules.v4
# On Debian/Ubuntu, install iptables-persistent
apt install iptables-persistent
# Rules auto-loaded from /etc/iptables/rules.v4 on boot
nftables: The Modern Replacement
nftables replaces iptables, ip6tables, arptables, and ebtables with a single tool. Its syntax is cleaner, and it supports atomic rule replacement (all rules are applied at once, so there is no window where rules are partially loaded):
#!/usr/sbin/nft -f
# /etc/nftables.conf
flush ruleset
table inet filter {
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
# ICMP rate limited
ip protocol icmp icmp type echo-request \
limit rate 5/second accept
ip6 nexthdr icmpv6 icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert } \
limit rate 5/second accept
# SSH rate limited
tcp dport 22 ct state new limit rate 3/minute accept
# Web traffic
tcp dport { 80, 443 } accept
# Custom application ports
tcp dport { 8080, 9090 } accept
# Logging (rate limited)
limit rate 5/minute log prefix "nft-drop: " level warn
}
chain forward {
type filter hook forward priority 0; policy drop;
# Docker will insert its rules here
}
chain output {
type filter hook output priority 0; policy accept;
}
}
# NAT table for port forwarding
table inet nat {
chain prerouting {
type nat hook prerouting priority -100;
tcp dport 8080 dnat to 192.168.1.100:80
}
chain postrouting {
type nat hook postrouting priority 100;
oifname "eth0" masquerade
}
}
# Apply rules
nft -f /etc/nftables.conf
# List all rules
nft list ruleset
# Add a rule interactively
nft add rule inet filter input tcp dport 3306 accept
# Delete a rule (by handle)
nft -a list chain inet filter input # Show handles
nft delete rule inet filter input handle 15
# Enable on boot
systemctl enable nftables
nftables Sets and Maps
One of nftables' most powerful features is named sets -- allowing you to group IPs, ports, or interfaces:
table inet filter {
set blocked_ips {
type ipv4_addr
flags timeout
elements = { 192.168.1.200 timeout 1h, 10.0.0.50 timeout 24h }
}
set allowed_ports {
type inet_service
elements = { 22, 80, 443, 8080 }
}
chain input {
type filter hook input priority 0; policy drop;
# Drop blocked IPs
ip saddr @blocked_ips drop
# Allow connections to allowed ports
tcp dport @allowed_ports accept
ct state established,related accept
iif lo accept
}
}
# Add to a set dynamically
nft add element inet filter blocked_ips { 10.0.0.100 timeout 2h }
# Remove from a set
nft delete element inet filter blocked_ips { 10.0.0.100 }
firewalld: Zone-Based Firewall
firewalld uses the concept of zones, where each zone represents a trust level. Network interfaces are assigned to zones, and rules are defined per zone:
# Check status
firewall-cmd --state
# List zones
firewall-cmd --get-zones
# List active zones
firewall-cmd --get-active-zones
# Show rules for a zone
firewall-cmd --zone=public --list-all
# Add a service
firewall-cmd --zone=public --add-service=http --permanent
firewall-cmd --zone=public --add-service=https --permanent
# Add a custom port
firewall-cmd --zone=public --add-port=8080/tcp --permanent
# Remove a service
firewall-cmd --zone=public --remove-service=ssh --permanent
# Reload to apply permanent rules
firewall-cmd --reload
# Rich rules for complex logic
firewall-cmd --zone=public --add-rich-rule='
rule family="ipv4" source address="192.168.1.0/24"
service name="ssh" accept' --permanent
# Rate limiting with rich rules
firewall-cmd --zone=public --add-rich-rule='
rule service name="http" limit value="100/m" accept' --permanent
# Port forwarding
firewall-cmd --zone=public --add-forward-port=\
port=8080:proto=tcp:toaddr=192.168.1.100:toport=80 --permanent
UFW: Uncomplicated Firewall
# Enable UFW
ufw enable
# Default policies
ufw default deny incoming
ufw default allow outgoing
# Allow SSH
ufw allow 22/tcp
# Allow HTTP/HTTPS
ufw allow 80/tcp
ufw allow 443/tcp
# Allow from specific IP
ufw allow from 192.168.1.0/24 to any port 22
# Rate limiting
ufw limit ssh
# Check status
ufw status verbose
# Delete a rule
ufw delete allow 80/tcp
# Application profiles
ufw app list
ufw allow 'Nginx Full'
Docker and Firewall Interaction
This is where many administrators get burned. Docker manipulates iptables/nftables directly to set up container networking, and it does so in ways that can bypass your firewall rules entirely:
-p 8080:80 makes that port accessible from the network regardless of your INPUT chain rules. Your carefully crafted firewall is effectively bypassed for container ports.
# See Docker's iptables rules
iptables -L DOCKER -n -v
iptables -L DOCKER-USER -n -v
iptables -t nat -L -n -v
Solution 1: Use DOCKER-USER Chain
Docker provides the DOCKER-USER chain specifically for user rules. Rules here are processed before Docker's own rules:
# Restrict access to Docker containers from external networks
iptables -I DOCKER-USER -i eth0 -j DROP
iptables -I DOCKER-USER -i eth0 -s 192.168.1.0/24 -j RETURN
iptables -I DOCKER-USER -i eth0 -m conntrack \
--ctstate ESTABLISHED,RELATED -j RETURN
Solution 2: Disable Docker iptables Management
# In /etc/docker/daemon.json
{
"iptables": false
}
# You must now manually configure NAT and forwarding for containers
# This is more work but gives you full control
Solution 3: Bind to Localhost Only
# Instead of publishing to all interfaces:
# docker run -p 8080:80 myapp
# Bind to localhost only:
docker run -p 127.0.0.1:8080:80 myapp
# Then use a reverse proxy (nginx/traefik) to expose services
# The reverse proxy handles TLS and is controlled by your firewall
Choosing the Right Tool
| Scenario | Recommended | Reason |
|---|---|---|
| Simple server (few rules) | UFW | Minimal syntax, quick to configure |
| Production server | nftables | Atomic loading, sets, modern syntax |
| RHEL/CentOS environment | firewalld | Default, well-integrated, zone model |
| Docker host | nftables + DOCKER-USER | Best control over Docker interaction |
| Legacy systems | iptables | Broadest compatibility |
| Multi-zone networks | firewalld or nftables | Zone model or sets for organized rules |
When managing Docker containers across multiple servers with usulnet, understanding the firewall landscape on each host is essential. Container port mappings interact directly with the host firewall, and misconfigurations can either expose services unintentionally or block them unexpectedly.
The golden rule of firewalling: Default deny, explicit allow. Start by dropping everything, then add rules only for traffic you explicitly need. Every open port is a potential attack surface. Every rule you add should be documented and justified.