Skip to main content
UFW vs firewalld comparison — Linux firewall management tools, zone model, and systemd integration

UFW vs firewalld: A Practical Comparison

Both UFW and firewalld are front-ends for Linux packet filtering — they translate human-readable rules into iptables or nftables directives — but they make fundamentally different assumptions about what a firewall configuration looks like and who is operating it. UFW is a simplicity-first tool designed for single-machine configurations where the common operations are "allow port 22" and "block everything else." firewalld is a dynamic, zone-aware service with D-Bus integration designed for environments where network context can change at runtime. This comparison sits within the technical notes section, relevant to anyone running Linux servers or workstations who needs to choose between the two or migrate between them. The IPv6 handling differences between the two tools connect directly to the problems described in the fail2ban IPv6 coverage entry. For users approaching Linux firewall management from a Windows background, the Linux on Windows topic provides broader context. The sections below cover architecture, zone concepts, systemd integration, practical command syntax, and the distribution defaults that often make the decision for you.


The short version

→ Short Answer

Use UFW on Ubuntu or Debian desktop and server systems where you want a simple, rule-based firewall you can configure in minutes. Use firewalld on RHEL, CentOS, Fedora, or any system where the distribution has adopted it as the default — fighting the default is more work than learning firewalld's zone model. On systems that will run Docker or other container runtimes, understand that both tools have interaction issues with Docker's iptables manipulation, though the symptoms differ. Neither tool is objectively superior; they are designed for different operational models.


Architecture overview

UFW (Uncomplicated Firewall)

UFW is a stateful firewall management tool that generates iptables rules (or nftables rules via an iptables compatibility shim, depending on the system configuration). It stores its rules as configuration files under /etc/ufw/ and applies them by regenerating the full rule set on each change. The model is stateless from the tool's perspective: you define a set of allow/deny rules, and UFW produces the iptables invocations to implement them.

The configuration is split across:

  • /etc/ufw/ufw.conf — global settings, whether UFW is enabled, default policies
  • /etc/ufw/user.rules — IPv4 rules as written by ufw commands
  • /etc/ufw/user6.rules — IPv6 equivalents (mirrored from IPv4 rules unless explicitly different)
  • /etc/ufw/before.rules and /etc/ufw/before6.rules — rules applied before user rules, including connection tracking
  • /etc/ufw/after.rules and /etc/ufw/after6.rules — rules applied after user rules

The before.rules file is where you make the raw iptables additions that UFW's simplified command syntax does not cover — NAT rules, specific LOG targets, complex multiport rules.

firewalld

firewalld is a daemon with a D-Bus interface that manages firewall rules dynamically. It does not regenerate the full rule set on each change; it applies incremental changes at runtime without disrupting existing connections. The core abstractions are:

  • Zones: Named trust levels (trusted, home, work, public, dmz, block, drop) with associated rule sets. Each network interface is assigned to a zone. Rules apply per zone.
  • Services: Named groups of ports and protocols (e.g., ssh = port 22/tcp, http = port 80/tcp) that can be added to or removed from zones by name.
  • Runtime vs permanent configuration: Changes can be applied immediately to the running firewall (runtime) or written to permanent configuration files (permanent), or both. --permanent changes survive reboots; runtime changes do not.
↻ What Changed

firewalld 0.6.0 (released 2018) added nftables as a native backend, replacing the iptables backend for distributions that adopted it. RHEL 8 / CentOS 8 uses the nftables backend by default. The nftables backend is not just a compatibility shim — it generates native nftables rules, which means the rule syntax in /etc/nftables.conf and the output of nft list ruleset reflects firewalld's managed rules directly.


Zone concepts in firewalld

The zone model is the most significant conceptual difference between firewalld and UFW. In UFW, there is one rule set and rules apply globally to all interfaces. In firewalld, each interface belongs to a zone, and the zone determines which rules apply.

The built-in zones, ordered roughly from most to least permissive:

ZoneDefault policyIntended use
trustedAccept allFully trusted network (internal, VPN)
homeAccept selected servicesHome network
workAccept selected servicesWork network
publicAccept ssh + dhcpv6-clientUntrusted public network
dmzAccept ssh onlyDMZ host exposed to internet
externalAccept ssh, masqueradeExternal interface with masquerading
internalAccept selected servicesInternal network
blockReject allBlock all incoming, allow outgoing
dropDrop allSilently drop all incoming

The public zone is the default zone for new interfaces. A freshly configured server with firewalld will allow SSH and DHCPv6 client traffic on any unconfigured interface, which is a reasonable default.

⚙ Compatibility Note

On a multi-homed server — one with both an internal NIC and a public-facing NIC — the zone model genuinely helps: assign the internal interface to internal or trusted and the external interface to public or dmz. UFW handles this via interface-specific rules (ufw allow in on eth0 from any to any port 22) but the configuration becomes less readable as complexity grows.


Command syntax comparison

The same common operations expressed in both tools:

Allow SSH

UFW — allow SSH
sudo ufw allow ssh
# or explicitly:
sudo ufw allow 22/tcp
firewalld — allow SSH in public zone (permanent)
sudo firewall-cmd --zone=public --add-service=ssh --permanent
sudo firewall-cmd --reload

Allow a specific port

UFW — allow port 8080/tcp
sudo ufw allow 8080/tcp
firewalld — allow port 8080/tcp
sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent
sudo firewall-cmd --reload

Block an IP address

UFW — block specific IP
sudo ufw deny from 203.0.113.42
firewalld — block specific IP using rich rule
sudo firewall-cmd --zone=public \
--add-rich-rule='rule family="ipv4" source address="203.0.113.42" drop' \
--permanent
sudo firewall-cmd --reload

View current rules

UFW — view rules
sudo ufw status verbose
sudo ufw status numbered
firewalld — view zone configuration
sudo firewall-cmd --list-all
sudo firewall-cmd --list-all-zones
# View runtime vs permanent:
sudo firewall-cmd --list-all --permanent
UFW and firewalld rule inspection output comparison showing status, zones, and active services

IPv6 handling

Both tools handle IPv6, but the implementation differs.

UFW: IPv6 support is controlled by the IPV6=yes setting in /etc/ufw/ufw.conf. When enabled, UFW maintains parallel user.rules (IPv4) and user6.rules (IPv6) rule sets. Most ufw allow commands automatically create both IPv4 and IPv6 rules. You can verify this by checking that port allows appear in both ufw status (which shows IPv4) and that ip6tables -L shows the corresponding rules.

⚠ Common Pitfall

If IPV6=yes is not set in /etc/ufw/ufw.conf, UFW will not create ip6tables rules, leaving IPv6 traffic unfiltered — neither blocked by the default deny policy nor protected by your allow rules. This is a silent misconfiguration because ufw status shows your IPv4 rules as active and correct. The gap only becomes apparent when you check ip6tables -L directly or test from an IPv6 address. This is the same class of silent IPv6 gap described in the context of fail2ban on the fail2ban IPv6 coverage page.

firewalld: IPv6 is handled transparently through the nftables backend (or ip6tables when using the iptables backend). Zone rules apply to both IPv4 and IPv6 traffic by default. Rich rules require specifying family="ipv4" or family="ipv6" explicitly to target one protocol family; rules without a family specifier apply to both.


systemd integration

UFW and systemd

UFW ships with a systemd service unit (ufw.service) that runs ufw start on boot. The integration is straightforward but limited — UFW is not dynamically aware of systemd service events; it loads rules at boot and that is the extent of the integration.

Enable UFW via systemd
sudo systemctl enable ufw
sudo systemctl start ufw
# Equivalent to:
sudo ufw enable

firewalld and systemd

firewalld is a daemon with deep systemd integration. It runs as firewalld.service, supports socket activation, and exposes a D-Bus API that other systemd services can use to request firewall rules on startup. NetworkManager uses this API to assign interfaces to zones when network connections come up — when you connect to a new WiFi network, NetworkManager can tell firewalld which zone to assign that connection to based on saved zone assignments.

firewalld service management
sudo systemctl enable firewalld
sudo systemctl start firewalld
# Check daemon status and loaded configuration:
sudo systemctl status firewalld
sudo firewall-cmd --state

The D-Bus interface also allows applications to request temporary port openings — useful for peer-to-peer applications, Avahi/mDNS service advertisement, and container runtimes that need to open ports dynamically. This is not a feature UFW exposes.


Distribution defaults

The default firewall tool is largely determined by your distribution:

DistributionDefault firewall tool
Ubuntu 20.04+UFW (inactive by default, pre-installed)
DebianUFW available, not pre-installed
RHEL 7/8/9firewalld (active by default)
CentOS 7/8/9firewalld (active by default)
Fedorafirewalld (active by default)
Arch LinuxNeither (install your choice)
openSUSEfirewalld

Running a distribution's non-default firewall tool is not wrong, but it creates friction: documentation, community support, automation tools, and integration scripts will assume the default. RHEL satellite management tools, cloud provider agent scripts, and application setup guides in the Red Hat ecosystem assume firewalld. Ubuntu documentation and tutorials assume UFW.

⬡ Observed Behaviour

On Ubuntu 22.04 cloud instances provisioned on AWS, GCP, and DigitalOcean, UFW is present but inactive by default. The cloud provider's security group or network ACL handles ingress filtering at the infrastructure level. Enabling UFW on a cloud instance without first ensuring SSH access is allowed in UFW rules will lock you out — the order of operations matters, and the default UFW deny-all policy takes effect the moment you run sudo ufw enable.


Docker and container runtime interactions

Both UFW and firewalld have well-known interaction issues with Docker, which manipulates iptables directly and bypasses both tools' managed rule sets for container port mappings.

UFW + Docker: Docker's default bridge network uses iptables NAT rules in the DOCKER chain, which is applied before UFW's ufw-user-forward chain. A ufw deny 8080 rule will not block access to a container published on port 8080 — Docker has already accepted the connection. The standard workaround involves modifying /etc/ufw/after.rules to add a DOCKER-USER chain rule, or configuring Docker with --iptables=false and managing container routing manually.

firewalld + Docker: Docker detects firewalld and can integrate with it, but the integration requires explicit configuration. Docker 20.10+ improved firewalld compatibility, but container port mappings still interact with firewalld zones in ways that require deliberate zone assignment rather than working transparently.


When each tool fits

Choose UFW when:

  • You are on Ubuntu or Debian and want to be done quickly
  • Your firewall requirements are a simple allow-list of ports
  • You are configuring a single-purpose server or personal workstation
  • The simplicity of ufw allow ssh is more valuable than architectural flexibility

Choose firewalld when:

  • You are on RHEL, CentOS, or Fedora (the choice has already been made)
  • You manage servers with multiple network interfaces that need different trust levels
  • You need dynamic rule changes at runtime without reloading the full rule set
  • Your automation or configuration management tooling (Ansible, Puppet) already has firewalld modules