Why choose
On Linux in 2026 there are four current ways to manage netfilter:
| Tooling | Level | Where it is default |
|---|---|---|
| cmd-iptables | low, legacy | Ubuntu LTS, embedded, Docker (still) |
| cmd-nft | low, modern | Debian 11+, Ubuntu 22+, direct kernel API |
| firewalld | high | RHEL/CentOS/Fedora server |
| ufw | high | Ubuntu desktop |
iptables vs nftables is a technical dilemma (legacy vs modern, rules-replay vs atomic). firewalld vs plain nft is an architectural dilemma: do you orchestrate yourself, or does a daemon do it. In this comparison ufw is a simplified firewalld for Ubuntu.
What firewalld is
A daemon that reads XML config files and applies them to netfilter
through a backend (since RHEL 8 that is nftables, before that iptables):
/usr/share/firewalld/ ← distribution default
/etc/firewalld/ ← customization
├─ firewalld.conf ← global config
├─ zones/ ← XML files describing zones
│ ├─ public.xml
│ ├─ trusted.xml
│ └─ work.xml
├─ services/ ← XML files describing services
│ ├─ ssh.xml
│ └─ http.xml
└─ icmptypes/
Commands go through firewall-cmd:
firewall-cmd --state # is it running
firewall-cmd --get-active-zones
firewall-cmd --get-default-zone
firewall-cmd --list-all # the current zone in detail
# Open HTTP permanently
firewall-cmd --permanent --add-service=http
firewall-cmd --reload
# An arbitrary port
firewall-cmd --permanent --add-port=8080/tcp
# Remove
firewall-cmd --permanent --remove-service=http
--permanent writes to XML; without it the change is runtime-only and is
lost on reload. A common pattern: first test without --permanent, then run
--runtime-to-permanent to commit.
Zones, the firewalld concept
Every network interface or source IP belongs to a zone. A zone is a trust level plus a list of allowed services and ports.
Default zones:
| Zone | By default |
|---|---|
| drop | drop everything, no replies |
| block | reject everything (with ICMP unreachable) |
| public | default; ssh and dhcpv6-client allowed |
| external | for NAT, MASQUERADE enabled |
| dmz | ssh allowed; for an external interface facing DMZ machines |
| work | ssh, dhcpv6-client, and mdns allowed |
| home | plus samba and mdns; for a home network |
| internal | same as home |
| trusted | everything allowed |
Binding an interface:
firewall-cmd --zone=trusted --change-interface=eth1 --permanent
Binding a source IP:
firewall-cmd --zone=trusted --add-source=10.0.0.0/24 --permanent
Sources take priority over interfaces. If the source falls into zone X, the traffic follows the rules of X regardless of the interface.
This is a strong point of firewalld: the same server can apply different rules to different networks without a complex rule cascade.
Rich rules, customization in firewalld
When services and ports are not enough, there are rich rules, a DSL for complex rules:
firewall-cmd --permanent --add-rich-rule='
rule family="ipv4"
source address="10.0.0.0/24"
service name="http"
log prefix="http-from-internal: " level="info"
accept'
firewall-cmd --permanent --add-rich-rule='
rule family="ipv4"
source address="1.2.3.4"
drop'
firewall-cmd --permanent --add-rich-rule='
rule
service name="ssh"
accept
limit value="3/m"' # rate-limit
Handy: ipset-style matching, rate-limit, log, and accept/reject/drop. Under the hood it still compiles to nftables.
Plain nft, Pro
Direct control:
# /etc/nftables.conf
table inet filter { set blocked_ips {type ipv4_addr
flags interval
elements = { 1.2.3.0/24, 5.6.7.8 }}
chain input {type filter hook input priority filter; policy drop;
ct state established,related accept
iif lo accept
ip saddr @blocked_ips drop
tcp dport { 22, 80, 443 } acceptip protocol icmp accept
counter log prefix "DROPPED: " drop
}
chain forward {type filter hook forward priority filter; policy drop;
}
}
Apply:
nft -f /etc/nftables.conf
systemctl enable --now nftables
Advantages over firewalld:
- Atomic reload: the whole file in one transaction, with no race window
- Sets and maps, natively (vlan-id to action, ip to action)
- One config in git, version controlled
- Fewer abstractions: what you wrote is what lands in the kernel
- Counters and log inline, with no extra plumbing
- Less overhead: there is no daemon
Drawbacks:
- No zones or services abstraction, everything is by hand
- No API for dynamic redrawing (each change is a file edit plus reload)
- Harder for junior engineers
When to choose what
firewalld:
- Desktop or laptop on different networks (home, work, coffee shop)
- A multi-tenant server with zones by trust
- RHEL production where admins are used to firewall-cmd
- You need integration with NetworkManager (NM zones)
- Dynamic rules through an API or D-Bus from applications
Plain nftables:
- A server fleet with config management (Ansible, Puppet)
- One config for a group of servers
- Performance-sensitive loads (gateway, k8s nodes)
- Complex rule sets with sets, maps, and vmaps
- Custom NAT and mangle rules
iptables-legacy:
- Only if something old does not work with nft
- Docker still writes its rules through iptables, so leave it there
ufw:
- The simple case of "allow SSH, allow HTTPS, drop everything else" on a single machine; for anything more, use firewalld or nft
Migration firewalld to nft
firewall-cmd --runtime-to-permanent # commit everything to XML
systemctl stop firewalld
systemctl disable firewalld
systemctl mask firewalld
nft list ruleset > /etc/nftables.conf # export
# Clean up, add a header, skip .data {} and .options {}systemctl enable --now nftables
Check that everything is open as before: nft list ruleset.
firewalld backend toggle
You can temporarily revert to the iptables backend if nft breaks something:
# /etc/firewalld/firewalld.conf
FirewallBackend=iptables # default is nftables
systemctl restart firewalld
Use this only if a specific case breaks on the nftables backend.
When things go wrong
firewall-cmd: not running: the service is not started. Runsystemctl start firewalld. Check whether it is masked (systemctl unmask firewalld).- Changes disappear after reload: you forgot
--permanent. Service conflicts: another firewall tool (iptables-services, ufw, nftables.service) is already running. Mask the extra ones.- NAT does not work in the external zone: masquerade is enabled by default
only in external. In public, run
firewall-cmd --zone=public --add-masquerade. nft: Could not process rule: No such file or directory: a syntax error in the config; nftables-services does not load. Checknft -c -f /etc/nftables.conf(dry run).- Docker broke after an nft restart: Docker writes its own rules
to iptables/nft at start. Restart
systemctl restart dockerafter the firewall. - You opened a port but it does not work: you forgot
firewall-cmd --reloadafter--permanent. Or the wrong zone (check--get-active-zones).
Comparison table
| Feature | firewalld | nftables (plain) | cmd-iptables | ufw |
|---|---|---|---|---|
| Style | declarative XML + CLI | declarative file | imperative CLI | declarative CLI |
| Backend | nftables (default) or iptables | nftables (kernel) | iptables (kernel xt_) | iptables |
| Zones | yes | no | no | no |
| Atomic reload | through restart | yes | no | no |
| Rich rules / DSL | rich-rules | nft DSL | shell-only | rules.before |
| API / D-Bus | yes | no | no | no |
| Runtime vs persistent | separated | one | by hand iptables-save | one |
| Default on | RHEL/Fedora | Debian/Ubuntu modern | embedded, Alpine, legacy | Ubuntu desktop |
| Complexity | medium | low (if you know nft) | medium | very low |