firewalld — Dynamic Firewall Reference

A dynamic firewall daemon for Linux — manages nftables (or iptables) rules at runtime through zones, services, and rich rules, with no need to reload the entire ruleset on every change.

zones · services · rich rules · NAT · RHEL · Fedora · Debian · Ubuntu
01 — What is firewalld?

firewalld is a firewall management layer that sits above nftables (default since RHEL 8) or iptables. Instead of writing raw packet-filter rules, you work with higher-level abstractions: zones that define trust levels, services that group ports by name, and rich rules for anything more nuanced.

firewalld's key advantage over raw iptables is runtime changes without a full reload — open a port and it takes effect immediately, without dropping existing connections.

Changes come in two flavours: runtime (active immediately, lost on reload or reboot) and permanent (written to disk, survive reboots but require --reload to activate). The recommended habit is to apply both every time.

ConceptMeaning
ZoneA named trust policy. Interfaces and source IPs are assigned to zones. Rules are per-zone.
ServiceA named group of ports/protocols (e.g. http = TCP 80). Reusable across zones.
PortA raw port/protocol pair opened directly on a zone without a service definition.
Rich ruleAn expressive rule language for source filtering, logging, rate-limiting, and more — within a zone.
Direct ruleA raw nftables/iptables rule passed straight through. Escape hatch for unsupported features.
Policyfirewalld 0.9+ construct for controlling forwarded traffic between zones.
02 — Installation
RHEL / Fedora / Rocky / AlmaLinux
sudo dnf install firewalld sudo systemctl enable --now firewalld
Debian / Ubuntu
sudo apt update && sudo apt install firewalld # Disable ufw first to avoid conflicts sudo systemctl disable --now ufw sudo systemctl enable --now firewalld
Arch Linux
sudo pacman -S firewalld sudo systemctl enable --now firewalld
Verify
# Check daemon status sudo firewall-cmd --state # Show version firewall-cmd --version # Show active zones and assigned interfaces sudo firewall-cmd --get-active-zones
⚠  Do not run firewalld alongside manually managed nftables or iptables rules. firewalld owns the ruleset — mixing the two causes conflicts and unpredictable behaviour.
03 — Zones

Every interface and source IP belongs to exactly one zone. firewalld ships nine predefined zones ordered from most to least trusted.

ZoneDefault policyTypical use
trustedAccept all trafficVPN tunnel, internal management network
homeAccept selected servicesHome LAN — trusts other local hosts
internalAccept selected servicesInternal servers — similar to home
workAccept selected servicesCorporate LAN
publicSSH + DHCP client onlyDefault for new interfaces. Internet-facing.
externalSSH only + masquerade onWAN interface when doing NAT routing
dmzSSH onlyDemilitarised zone — limited inbound access
blockReject all inbound (ICMP)Block everything with a rejection notice
dropDrop all inbound silentlyStrictest — no response to any inbound packet
Zone management
# List all available zones firewall-cmd --get-zones # Show current default zone firewall-cmd --get-default-zone # Change default zone sudo firewall-cmd --set-default-zone=dmz # Show full config of a zone (runtime) sudo firewall-cmd --zone=public --list-all # Show all zones and their config sudo firewall-cmd --list-all-zones # Assign an interface to a zone permanently sudo firewall-cmd --zone=internal --change-interface=eth1 --permanent sudo firewall-cmd --reload # Assign a source IP / CIDR to a zone sudo firewall-cmd --zone=trusted --add-source=10.0.0.0/8 --permanent sudo firewall-cmd --reload # Remove a source from a zone sudo firewall-cmd --zone=trusted --remove-source=10.0.0.0/8 --permanent sudo firewall-cmd --reload
ℹ  Source-based zone assignment takes precedence over interface-based. An IP in the trusted zone is trusted regardless of which interface its packets arrive on.
04 — Services

Services are named port bundles defined in XML files under /usr/lib/firewalld/services/. Using service names is cleaner than raw ports — they self-document and group related ports (e.g. ftp opens TCP 20 and 21 together).

Service management
# List all available service names firewall-cmd --get-services # Show what ports a service includes firewall-cmd --info-service=https firewall-cmd --info-service=samba # Add a service — runtime sudo firewall-cmd --zone=public --add-service=http # Add a service — permanent + reload to activate sudo firewall-cmd --zone=public --add-service=https --permanent sudo firewall-cmd --reload # Remove a service sudo firewall-cmd --zone=public --remove-service=http --permanent sudo firewall-cmd --reload # List active services in a zone sudo firewall-cmd --zone=public --list-services
Common services reference
Service namePorts opened
sshTCP 22
httpTCP 80
httpsTCP 443
ftpTCP 20, 21
ftpsTCP 989, 990
smtpTCP 25
smtp-submissionTCP 587
smtpsTCP 465
imapTCP 143
imapsTCP 993
pop3TCP 110
pop3sTCP 995
dnsTCP + UDP 53
ntpUDP 123
mysqlTCP 3306
postgresqlTCP 5432
redisTCP 6379
sambaTCP 139, 445 · UDP 137, 138
openvpnUDP 1194
wireguardUDP 51820
cockpitTCP 9090
grafanaTCP 3000
Creating a custom service
# Create the service definition sudo firewall-cmd --new-service=myapp --permanent sudo firewall-cmd --service=myapp --add-port=8080/tcp --permanent sudo firewall-cmd --service=myapp --add-port=8443/tcp --permanent sudo firewall-cmd --service=myapp --set-description="My Application API" --permanent # Use it like any built-in service sudo firewall-cmd --zone=public --add-service=myapp --permanent sudo firewall-cmd --reload
05 — Opening Ports Directly
# Open a single TCP port sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent sudo firewall-cmd --reload # Open a UDP port sudo firewall-cmd --zone=public --add-port=51820/udp --permanent sudo firewall-cmd --reload # Open a port range (e.g. passive FTP data) sudo firewall-cmd --zone=public --add-port=60000-61000/tcp --permanent sudo firewall-cmd --reload # Remove a port sudo firewall-cmd --zone=public --remove-port=8080/tcp --permanent sudo firewall-cmd --reload # List all open ports in a zone sudo firewall-cmd --zone=public --list-ports
06 — Rich Rules

Rich rules give you per-source filtering, logging, rate limiting, and custom reject actions — all within the zone model, without touching raw nftables syntax.

Rich rule syntax
rule [family="ipv4|ipv6"] [source address="ip/cidr" [invert="true"]] [destination address="ip/cidr"] [service name="svc" | port port="n" protocol="tcp|udp"] [log [prefix="text"] [level="info|warn|err"] [limit value="n/s|m|h|d"]] [accept | drop | reject [type="icmp-type"]]
Examples
# Allow SSH from a specific subnet only sudo firewall-cmd --zone=public \ --add-rich-rule='rule family="ipv4" source address="203.0.113.0/24" service name="ssh" accept' \ --permanent # Block all traffic from a specific IP sudo firewall-cmd --zone=public \ --add-rich-rule='rule family="ipv4" source address="198.51.100.99" drop' \ --permanent # Rate-limit SSH to 3 new connections per minute, log each attempt sudo firewall-cmd --zone=public \ --add-rich-rule='rule service name="ssh" log prefix="SSH: " level="info" limit value="3/m" accept' \ --permanent # Allow HTTP only from an internal CIDR with logging sudo firewall-cmd --zone=public \ --add-rich-rule='rule family="ipv4" source address="10.0.0.0/8" service name="http" log prefix="HTTP-INT: " accept' \ --permanent # Reject SMTP with an ICMP admin-prohibited message sudo firewall-cmd --zone=public \ --add-rich-rule='rule service name="smtp" reject type="icmp-admin-prohibited"' \ --permanent # Allow a specific port from a specific IP sudo firewall-cmd --zone=public \ --add-rich-rule='rule family="ipv4" source address="10.0.0.5" port port="9200" protocol="tcp" accept' \ --permanent sudo firewall-cmd --reload
List and remove rich rules
# List all rich rules in a zone sudo firewall-cmd --zone=public --list-rich-rules # Remove a rich rule (exact string match required) sudo firewall-cmd --zone=public \ --remove-rich-rule='rule family="ipv4" source address="198.51.100.99" drop' \ --permanent sudo firewall-cmd --reload
07 — Runtime vs Permanent

Every change can target the runtime config, the permanent config, or both. This is the most common source of confusion with firewalld.

ModeFlagTakes effectSurvives reboot
Runtime onlyno flagImmediatelyNo — lost on reload or reboot
Permanent only--permanentAfter --reloadYes
BothRun command twice — with and without --permanentImmediately + persistedYes
Recommended pattern
# Apply runtime immediately, then persist sudo firewall-cmd --zone=public --add-service=https sudo firewall-cmd --zone=public --add-service=https --permanent # Or: add permanent, then reload to activate sudo firewall-cmd --zone=public --add-service=https --permanent sudo firewall-cmd --reload # Reload — applies permanent config, keeps existing connections alive sudo firewall-cmd --reload # Compare runtime vs permanent side by side sudo firewall-cmd --zone=public --list-all sudo firewall-cmd --zone=public --list-all --permanent
⚠  If a rule works now but vanishes after reboot, you forgot --permanent. If a permanent rule never activates, you forgot --reload. These two mistakes account for almost every firewalld complaint.
08 — NAT & Port Forwarding
Masquerade (internet sharing / SNAT)
# Enable masquerade on the internet-facing zone sudo firewall-cmd --zone=external --add-masquerade --permanent sudo firewall-cmd --reload # Verify sudo firewall-cmd --zone=external --query-masquerade # Enable kernel IP forwarding (required) echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-forward.conf sudo sysctl -p /etc/sysctl.d/99-forward.conf
Port forwarding (DNAT)
# Forward external TCP:80 to an internal host on the same port sudo firewall-cmd --zone=external \ --add-forward-port=port=80:proto=tcp:toaddr=192.168.1.10 \ --permanent # Forward external TCP:8080 to internal host on port 80 sudo firewall-cmd --zone=external \ --add-forward-port=port=8080:proto=tcp:toaddr=192.168.1.10:toport=80 \ --permanent # Local redirect — port 80 to 8080 on this same host sudo firewall-cmd --zone=public \ --add-forward-port=port=80:proto=tcp:toport=8080 \ --permanent sudo firewall-cmd --reload # List all port forwards in a zone sudo firewall-cmd --zone=external --list-forward-ports
09 — Common Service Configurations
Web server (HTTP + HTTPS)
sudo firewall-cmd --zone=public --add-service=http --permanent sudo firewall-cmd --zone=public --add-service=https --permanent sudo firewall-cmd --reload
SSH on a non-standard port
# Open the new port BEFORE removing the old one sudo firewall-cmd --zone=public --add-port=2222/tcp --permanent sudo firewall-cmd --reload # Verify you can connect on 2222, then remove the default SSH service sudo firewall-cmd --zone=public --remove-service=ssh --permanent sudo firewall-cmd --reload # On RHEL / SELinux — tell SELinux about the new port sudo semanage port -a -t ssh_port_t -p tcp 2222
Mail server (full stack)
sudo firewall-cmd --zone=public \ --add-service=smtp \ --add-service=smtp-submission \ --add-service=smtps \ --add-service=imap \ --add-service=imaps \ --add-service=pop3s \ --permanent sudo firewall-cmd --reload
Database — restricted to an app subnet
# Assign the app subnet to the internal zone sudo firewall-cmd --zone=internal --add-source=10.0.0.0/24 --permanent # Allow MySQL only within that zone sudo firewall-cmd --zone=internal --add-service=mysql --permanent sudo firewall-cmd --reload
WireGuard VPN
sudo firewall-cmd --zone=public --add-service=wireguard --permanent # On older versions without the built-in service: sudo firewall-cmd --zone=public --add-port=51820/udp --permanent sudo firewall-cmd --reload
Docker network integration
# Dedicated zone for Docker traffic prevents interface conflicts sudo firewall-cmd --new-zone=docker --permanent sudo firewall-cmd --zone=docker --add-interface=docker0 --permanent sudo firewall-cmd --zone=docker --set-target=ACCEPT --permanent sudo firewall-cmd --reload
10 — Inter-Zone Policies

Introduced in firewalld 0.9, policies control traffic forwarded between zones — for example, letting LAN hosts reach the internet through a router, or preventing DMZ hosts from reaching the LAN.

Creating policies
# Allow LAN (internal) to reach the internet (external) sudo firewall-cmd --new-policy=lan-to-wan --permanent sudo firewall-cmd --policy=lan-to-wan --add-ingress-zone=internal --permanent sudo firewall-cmd --policy=lan-to-wan --add-egress-zone=external --permanent sudo firewall-cmd --policy=lan-to-wan --set-target=ACCEPT --permanent # Allow DMZ outbound only — cannot reach internal zone sudo firewall-cmd --new-policy=dmz-to-wan --permanent sudo firewall-cmd --policy=dmz-to-wan --add-ingress-zone=dmz --permanent sudo firewall-cmd --policy=dmz-to-wan --add-egress-zone=external --permanent sudo firewall-cmd --policy=dmz-to-wan --set-target=ACCEPT --permanent sudo firewall-cmd --reload # List active policies sudo firewall-cmd --get-active-policies
11 — Panic Mode & Lockdown

Panic mode drops all traffic instantly — a break-glass emergency control. Lockdown restricts which applications on the host can modify the firewall at runtime.

Panic mode
# Drops ALL inbound and outbound traffic immediately # WARNING: severs all active connections including your SSH session sudo firewall-cmd --panic-on # Check status sudo firewall-cmd --query-panic # Disable (requires console / out-of-band access if SSH was dropped) sudo firewall-cmd --panic-off
Lockdown mode
sudo firewall-cmd --lockdown-on sudo firewall-cmd --query-lockdown sudo firewall-cmd --lockdown-off # Whitelist root to allow modification sudo firewall-cmd --add-lockdown-whitelist-uid=0
12 — Troubleshooting
SymptomCause & Fix
Rule active now, gone after rebootMissing --permanent. Re-add with the flag then run --reload.
Permanent rule not taking effectForgot --reload after adding it.
firewall-cmd hangs or times outDaemon not running — sudo systemctl start firewalld.
Port open but connection refusedThe service itself isn't listening, or SELinux is blocking it — check ausearch -m avc.
Conflict with Docker / libvirtAssign their interfaces to a permissive zone or create a dedicated ACCEPT-target zone.
Rich rule not matchingVerify family="ipv4" vs ipv6 and check order with --list-rich-rules.
Zone assignment not workingSource-based zone beats interface-based — check --get-active-zones.
Locked out after removing ssh serviceUse out-of-band console. Run firewall-cmd --zone=public --add-service=ssh (no --permanent) to restore access.
Diagnostic commands
# Daemon status sudo firewall-cmd --state # Which zone is this interface currently in? sudo firewall-cmd --get-zone-of-interface=eth0 # Full runtime config sudo firewall-cmd --list-all # Full permanent (on-disk) config sudo firewall-cmd --list-all --permanent # Watch the firewalld log live sudo journalctl -u firewalld -f # See the raw nftables rules firewalld generated underneath sudo nft list ruleset
13 — Quick Reference
CommandWhat it does
firewall-cmd --stateCheck if daemon is running
firewall-cmd --get-active-zonesShow zones with assigned interfaces and sources
firewall-cmd --zone=public --list-allFull runtime config of a zone
firewall-cmd --zone=public --list-all --permanentFull saved (on-disk) config of a zone
firewall-cmd --zone=public --add-service=https --permanentOpen HTTPS permanently
firewall-cmd --zone=public --remove-service=http --permanentClose HTTP permanently
firewall-cmd --zone=public --add-port=8080/tcp --permanentOpen port 8080 TCP permanently
firewall-cmd --zone=public --add-rich-rule='...' --permanentAdd a rich rule permanently
firewall-cmd --zone=external --add-masquerade --permanentEnable NAT masquerade on a zone
firewall-cmd --reloadApply permanent config without dropping connections
firewall-cmd --panic-on / --panic-offDrop / restore all traffic (emergency)
firewall-cmd --info-service=sshShow what ports a service definition includes
firewall-cmd --get-servicesList all available service names
✔  Always run firewall-cmd --list-all and firewall-cmd --list-all --permanent side by side to confirm runtime and on-disk config match. Divergence between the two is the root cause of most firewalld surprises.