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.
| Concept | Meaning |
| Zone | A named trust policy. Interfaces and source IPs are assigned to zones. Rules are per-zone. |
| Service | A named group of ports/protocols (e.g. http = TCP 80). Reusable across zones. |
| Port | A raw port/protocol pair opened directly on a zone without a service definition. |
| Rich rule | An expressive rule language for source filtering, logging, rate-limiting, and more — within a zone. |
| Direct rule | A raw nftables/iptables rule passed straight through. Escape hatch for unsupported features. |
| Policy | firewalld 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.
| Zone | Default policy | Typical use |
| trusted | Accept all traffic | VPN tunnel, internal management network |
| home | Accept selected services | Home LAN — trusts other local hosts |
| internal | Accept selected services | Internal servers — similar to home |
| work | Accept selected services | Corporate LAN |
| public | SSH + DHCP client only | Default for new interfaces. Internet-facing. |
| external | SSH only + masquerade on | WAN interface when doing NAT routing |
| dmz | SSH only | Demilitarised zone — limited inbound access |
| block | Reject all inbound (ICMP) | Block everything with a rejection notice |
| drop | Drop all inbound silently | Strictest — 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 name | Ports opened |
| ssh | TCP 22 |
| http | TCP 80 |
| https | TCP 443 |
| ftp | TCP 20, 21 |
| ftps | TCP 989, 990 |
| smtp | TCP 25 |
| smtp-submission | TCP 587 |
| smtps | TCP 465 |
| imap | TCP 143 |
| imaps | TCP 993 |
| pop3 | TCP 110 |
| pop3s | TCP 995 |
| dns | TCP + UDP 53 |
| ntp | UDP 123 |
| mysql | TCP 3306 |
| postgresql | TCP 5432 |
| redis | TCP 6379 |
| samba | TCP 139, 445 · UDP 137, 138 |
| openvpn | UDP 1194 |
| wireguard | UDP 51820 |
| cockpit | TCP 9090 |
| grafana | TCP 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.
| Mode | Flag | Takes effect | Survives reboot |
| Runtime only | no flag | Immediately | No — lost on reload or reboot |
| Permanent only | --permanent | After --reload | Yes |
| Both | Run command twice — with and without --permanent | Immediately + persisted | Yes |
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
| Symptom | Cause & Fix |
| Rule active now, gone after reboot | Missing --permanent. Re-add with the flag then run --reload. |
| Permanent rule not taking effect | Forgot --reload after adding it. |
| firewall-cmd hangs or times out | Daemon not running — sudo systemctl start firewalld. |
| Port open but connection refused | The service itself isn't listening, or SELinux is blocking it — check ausearch -m avc. |
| Conflict with Docker / libvirt | Assign their interfaces to a permissive zone or create a dedicated ACCEPT-target zone. |
| Rich rule not matching | Verify family="ipv4" vs ipv6 and check order with --list-rich-rules. |
| Zone assignment not working | Source-based zone beats interface-based — check --get-active-zones. |
| Locked out after removing ssh service | Use 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
| Command | What it does |
| firewall-cmd --state | Check if daemon is running |
| firewall-cmd --get-active-zones | Show zones with assigned interfaces and sources |
| firewall-cmd --zone=public --list-all | Full runtime config of a zone |
| firewall-cmd --zone=public --list-all --permanent | Full saved (on-disk) config of a zone |
| firewall-cmd --zone=public --add-service=https --permanent | Open HTTPS permanently |
| firewall-cmd --zone=public --remove-service=http --permanent | Close HTTP permanently |
| firewall-cmd --zone=public --add-port=8080/tcp --permanent | Open port 8080 TCP permanently |
| firewall-cmd --zone=public --add-rich-rule='...' --permanent | Add a rich rule permanently |
| firewall-cmd --zone=external --add-masquerade --permanent | Enable NAT masquerade on a zone |
| firewall-cmd --reload | Apply permanent config without dropping connections |
| firewall-cmd --panic-on / --panic-off | Drop / restore all traffic (emergency) |
| firewall-cmd --info-service=ssh | Show what ports a service definition includes |
| firewall-cmd --get-services | List 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.