Fedora Firewalls: The Essentials
Fedora uses firewalld as its firewall manager. Under the hood it's nftables (or iptables on older systems), but you interact with firewalld's higher-level concepts.
The Core Concept: Zones
A zone is a trust level. You assign network interfaces (and/or source IPs) to zones, and each zone has its own rules for what's allowed.
This is the mental model: "Wi-Fi at home is trusted, but Wi-Fi at a coffee shop is untrusted." Zones let you apply different rules to each.
Built-in zones, from most to least trusting
| Zone | Default behavior | Typical use |
|---|---|---|
trusted |
Accepts everything | VPN tunnels, loopback-equivalent |
home |
Accepts common home services (SSH, mDNS, Samba) | Home network |
internal |
Like home, for internal networks | Internal corporate |
work |
Accepts some services | Work network |
public |
Accepts only a few services | Default for unknown networks |
external |
Used for NAT gateway (router-like role) | When machine is acting as a router |
dmz |
Limited inbound, typical "DMZ host" posture | Servers in a DMZ |
block |
Rejects all incoming with ICMP message | "I'm here but go away" |
drop |
Silently drops all incoming | "Pretend I don't exist" |
Fedora also ships two custom zones:
FedoraWorkstation— default on Fedora Workstation; permissive for desktop useFedoraServer— default on Fedora Server; more restrictive
How traffic gets sorted into a zone
When a packet arrives, firewalld picks a zone by checking in this order:
- Source IP match — if a zone has a matching source range, use that zone
- Interface match — if a zone has the arriving interface bound, use that zone
- Default zone — otherwise, use the default zone
Rules of that zone are then applied. This is why "which zone is active" matters so much — rules in zones with no interfaces or sources bound are simply never evaluated.
Runtime vs Permanent — the #1 gotcha
firewalld has two config states:
- Runtime — live, in-memory. Changes take effect immediately. Lost on reload/reboot.
- Permanent — written to disk. Applied on reload/reboot, not immediately.
# Runtime (temporary, for testing)
sudo firewall-cmd --zone=public --add-service=http
# Permanent (written to disk, needs reload to activate)
sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --reloadRule of thumb: always use --permanent and follow with --reload. Otherwise your rule disappears on reboot.
To check if runtime and permanent match:
sudo firewall-cmd --zone=<zone> --list-all # runtime
sudo firewall-cmd --permanent --zone=<zone> --list-all # permanentDifferences between the two are usually a sign someone forgot --permanent.
Services vs Ports — two ways to allow traffic
Services are named bundles of ports/protocols. firewalld ships hundreds of predefined ones:
sudo firewall-cmd --get-services # list all available
sudo firewall-cmd --info-service=ssh # details of onessh is just a service that maps to TCP port 22. Using services is more readable than raw ports.
Ports are direct numeric rules:
sudo firewall-cmd --permanent --zone=public --add-port=8080/tcpUse services when one exists, ports when you need something custom. Both work identically under the hood.
Essential Commands — The Hard and Fast Reference
See what's happening
# Which zones have interfaces/sources bound right now
sudo firewall-cmd --get-active-zones
# Default zone (for interfaces that don't match any zone)
sudo firewall-cmd --get-default-zone
# Full config of one zone
sudo firewall-cmd --zone=<zone> --list-all
# All zones at once
sudo firewall-cmd --list-all-zones
# Just the interfaces, services, or ports
sudo firewall-cmd --zone=<zone> --list-interfaces
sudo firewall-cmd --zone=<zone> --list-services
sudo firewall-cmd --zone=<zone> --list-portsAdd rules
# Allow a named service
sudo firewall-cmd --permanent --zone=<zone> --add-service=<name>
# Allow a port
sudo firewall-cmd --permanent --zone=<zone> --add-port=<num>/<proto>
# Bind an interface to a zone
sudo firewall-cmd --permanent --zone=<zone> --add-interface=<iface>
# Trust traffic from a specific IP/subnet (via source match)
sudo firewall-cmd --permanent --zone=<zone> --add-source=<ip-or-cidr>
# Apply permanent changes
sudo firewall-cmd --reloadRemove rules
Same as add, but --remove- instead of --add-:
sudo firewall-cmd --permanent --zone=<zone> --remove-service=<name>
sudo firewall-cmd --permanent --zone=<zone> --remove-port=<num>/<proto>
sudo firewall-cmd --permanent --zone=<zone> --remove-interface=<iface>
sudo firewall-cmd --reloadChange defaults
# Change the default zone
sudo firewall-cmd --set-default-zone=<zone>
# Move an interface between zones
sudo firewall-cmd --permanent --zone=<new-zone> --change-interface=<iface>
sudo firewall-cmd --reloadAdvanced: Rich Rules
When services and ports aren't expressive enough (e.g. "allow SSH from only this subnet"):
sudo firewall-cmd --permanent --zone=public --add-rich-rule='
rule family="ipv4" source address="192.168.1.0/24" service name="ssh" accept'
sudo firewall-cmd --reloadRich rules can match by source, destination, service, port, log, limit rate, and more. Powerful but verbose. Avoid unless you actually need the flexibility.
Advanced: Forwarding and Masquerading
Masquerading is NAT — source address rewriting so traffic from an internal network appears to come from this machine. Turn it on only if this machine is acting as a router/gateway:
sudo firewall-cmd --permanent --zone=<zone> --add-masqueradeFor a typical desktop or single-purpose server, leave this off.
Port forwarding (redirecting incoming traffic to another machine/port):
sudo firewall-cmd --permanent --zone=<zone> \
--add-forward-port=port=8080:proto=tcp:toport=80:toaddr=192.168.1.50Also rare on a regular machine — usually you want this on your router, not on a Fedora host.
The NetworkManager Connection
On Fedora (and most modern distros), NetworkManager decides which zone each interface belongs to, not firewalld directly. That's why you sometimes see a zone's permanent config with empty interfaces: even though the interface is clearly bound at runtime.
Check per-connection zone assignment:
nmcli -f connection.id,connection.zone connection showSet one explicitly:
sudo nmcli connection modify "<connection-name>" connection.zone <zone>
sudo nmcli connection up "<connection-name>"This is the proper way to persistently bind an interface to a zone on Fedora Workstation. It survives reboots and reconnects.
Troubleshooting Playbook
When something doesn't work, check in this order:
# 1. Is firewalld running?
sudo systemctl status firewalld
# 2. Which zone is the interface in right now?
sudo firewall-cmd --get-active-zones
# 3. What does that zone allow?
sudo firewall-cmd --zone=<zone> --list-all
# 4. Is the service actually listening?
sudo ss -tulnp | grep <port>
# 5. Is traffic arriving at all?
sudo tcpdump -i any -n port <port>
# 6. Runtime vs permanent consistent?
sudo firewall-cmd --zone=<zone> --list-all
sudo firewall-cmd --permanent --zone=<zone> --list-allMost real-world issues are one of:
- Rule added to runtime only, lost after reboot
- Rule added to wrong zone (inactive zone = rule never applied)
- Service not actually listening (firewall would let it through if it existed)
- Something upstream (router, ISP, CGNAT) blocking before packets arrive
The Minimalist Hardening Template
For a typical Fedora desktop or small server that only needs specific services exposed:
# 1. Confirm active zone
sudo firewall-cmd --get-active-zones
# 2. Start clean: remove anything you didn't add deliberately
sudo firewall-cmd --zone=<active-zone> --list-services
sudo firewall-cmd --zone=<active-zone> --list-ports
# Review the output, remove what you don't need with --remove-service / --remove-port
# 3. Add only what you actually use
sudo firewall-cmd --permanent --zone=<active-zone> --add-service=dhcpv6-client
# (add other specific needs here)
sudo firewall-cmd --reloadFor WireGuard setups specifically, bind wg0 to the trusted zone — traffic that's already authenticated by cryptographic keys doesn't need further firewall scrutiny:
sudo firewall-cmd --permanent --zone=trusted --add-interface=wg0
sudo firewall-cmd --reloadKey Mental Model
Remember these four things and you'll understand 90% of firewalld in practice:
- A zone is a trust level with its own rules. Interfaces and source IPs get sorted into zones.
- Active zones matter, inactive ones don't. A rule in a zone with no bound interfaces or sources is dead weight.
--permanentor it didn't happen. Always pair with--reload.- NetworkManager controls interface-to-zone binding on Fedora. Use
nmcli connection modify ... connection.zonefor persistence.
Everything else is just commands you can look up. These four concepts are what actually matter.
Cheat Sheet
# Show state
firewall-cmd --get-active-zones
firewall-cmd --get-default-zone
firewall-cmd --zone=<z> --list-all
# Add (always permanent)
firewall-cmd --permanent --zone=<z> --add-service=<s>
firewall-cmd --permanent --zone=<z> --add-port=<n>/<p>
firewall-cmd --permanent --zone=<z> --add-interface=<i>
firewall-cmd --permanent --zone=<z> --add-source=<cidr>
# Remove
firewall-cmd --permanent --zone=<z> --remove-service=<s>
firewall-cmd --permanent --zone=<z> --remove-port=<n>/<p>
# Apply
firewall-cmd --reload
# Runtime vs permanent diff
firewall-cmd --zone=<z> --list-all # runtime
firewall-cmd --permanent --zone=<z> --list-all # permanent
# NetworkManager zone binding
nmcli -f connection.id,connection.zone connection show
nmcli connection modify "<name>" connection.zone <z>That's the whole thing. Firewalld isn't complicated once you grasp zones and the runtime/permanent split — everything else falls out of those two ideas.