How to Install and Configure a Firewall on Fedora

Fedora ships with firewalld as its default firewall; install it if missing, then use firewall-cmd to open ports and manage zones for your network interfaces.

You just deployed a service and the network is silent

You compiled a web application and bound it to port 8080. You curl localhost and the response returns instantly. You try to curl the machine from your laptop and the request hangs until it times out. The application is running. The network cable is plugged in. The firewall is standing between you and your server.

What's actually happening

Fedora ships with firewalld as the default firewall daemon. It does not manage raw iptables or nftables chains directly. It organizes network traffic into zones. Each zone defines a set of rules for incoming and outgoing connections. NetworkManager assigns interfaces to zones automatically based on the network profile. When you plug into a coffee shop, the interface drops into the public zone. When you connect to your home router, it moves to home. The firewall evaluates packets against the zone assigned to the receiving interface.

The configuration model splits into two layers. The runtime configuration lives in memory and applies immediately. The persistent configuration lives on disk in XML files under /etc/firewalld/ and /usr/lib/firewalld/. Changes made without the --permanent flag vanish on reboot. Changes made with --permanent do not take effect until you reload the daemon. This split prevents accidental lockouts during active configuration. You can test a rule in the runtime layer, verify it works, and then commit it to disk. If the rule breaks your workflow, a reboot restores the previous state.

Run journalctl -xeu firewalld to see how the daemon processes zone changes. The x flag adds explanatory text and the e flag jumps to the end. Most sysadmins type this muscle-memory style when debugging network services.

Check the daemon status before you start configuring rules.

Here is how to verify the daemon status and start it if needed.

sudo firewall-cmd --state
# Returns "running" if active. Returns nothing if stopped.
sudo dnf install firewalld
# Installs the daemon and its XML service definitions.
sudo systemctl enable --now firewalld
# Starts the service immediately and marks it for boot.

Identify which zone your active interface belongs to. NetworkManager usually handles this, but verifying prevents applying rules to the wrong network segment.

Here is how to map interfaces to their current zones.

sudo firewall-cmd --get-active-zones
# Lists zones and the interfaces or sources assigned to them.
sudo firewall-cmd --list-all
# Dumps the full rule set for the default zone.

Query the runtime state first. Guessing the zone wastes time.

The fix or how-to

Opening a port requires two steps. You write the rule to the persistent configuration, then you reload the daemon to push it into the runtime layer. Skipping the reload leaves the rule dormant until the next reboot.

Here is how to open a TCP port permanently.

sudo firewall-cmd --permanent --add-port=8080/tcp
# Writes the port rule to /etc/firewalld/zones/public.xml.
sudo firewall-cmd --reload
# Applies persistent changes to the runtime without dropping connections.

Raw ports work, but predefined services are safer. The service definitions bundle the correct port, protocol, and sometimes module dependencies. They also update automatically when upstream changes the default port for a protocol. The XML files live in /usr/lib/firewalld/services/. Never edit files in /usr/lib/. Copy them to /etc/firewalld/services/ if you need to modify the definition. The package manager will overwrite /usr/lib/ on the next update.

Here is how to allow a standard service instead of a raw port.

sudo firewall-cmd --permanent --add-service=https
# Adds the https service definition, which maps to port 443/tcp.
sudo firewall-cmd --reload
# Pushes the service rule into the active firewall state.

You can list every service definition that ships with the base system. The list includes ssh, http, dhcpv6-client, mdns, and dozens of others.

Here is how to view available service definitions.

sudo firewall-cmd --get-services
# Prints a space-separated list of all built-in service XML files.

Advanced filtering requires rich rules. Rich rules let you target specific source IPs, ports, protocols, and log levels in a single statement. The syntax is XML-like but passed as a quoted string. The parser expects attributes in a strict order. Family comes first, then source or destination, then the action.

Here is how to block a specific IPv4 address permanently.

sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.0.113.42" reject'
# Creates a rule that drops packets from that IP with a rejection message.
sudo firewall-cmd --reload
# Activates the rich rule in the current session.

You can also forward ports or log specific traffic. The log action writes to the journal before applying the accept or reject action. This helps you verify that the rule matches the intended traffic before you commit to a drop.

Here is how to log and then accept traffic on a custom port.

sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" port port="9090" protocol="tcp" log prefix="custom-app: " level="info" accept'
# Logs matching packets with a custom prefix before allowing them.
sudo firewall-cmd --reload
# Pushes the logging rule into the active state.

Run firewall-cmd --reload after every rule change. Otherwise the runtime config and the persistent config diverge.

Verify it worked

Do not assume the rule applied. Query the runtime configuration directly. The persistent config and runtime config can diverge if you edit XML files manually or run commands without --permanent.

Here is how to confirm the active rules match your intent.

sudo firewall-cmd --list-all-zones
# Shows every zone and its currently active ports, services, and rules.
sudo firewall-cmd --query-port=8080/tcp
# Returns "yes" or "no" for the runtime state of that port.

Cross-reference the output with your application logs. If the firewall allows the traffic but the service still refuses connections, the problem is in the application binding or SELinux policy. Check journalctl -t setroubleshoot for one-line SELinux denial summaries. Read those before disabling SELinux.

Run the query command immediately after reload. Trust the output, not your memory.

Common pitfalls and what the error looks like

Forgetting --reload is the most common mistake. You add a permanent rule, test it, and see nothing change. The rule is sitting on disk waiting for a reboot. Always reload after persistent changes.

Mixing runtime and permanent commands causes confusion. Running firewall-cmd --add-port=8080/tcp without --permanent applies the rule immediately but discards it on reload or reboot. Use this only for temporary testing. If you leave it out of the permanent config, your server will lock you out after maintenance.

Rich rule syntax errors break the entire command. The daemon returns a clear error when the XML structure is malformed.

Error: INVALID_RICH_RULE: rule family="ipv4" source address="203.0.113.42" reject

The error points to a missing closing quote or a misplaced attribute. Wrap the entire rule in single quotes. Keep the XML attributes in the exact order the parser expects.

Editing XML files directly in /etc/firewalld/zones/ bypasses the daemon's validation. The next --reload will fail with a schema error. Use firewall-cmd or firewall-config to modify rules. Trust the package manager. Manual file edits drift, snapshots stay.

Check the journal before rewriting rules. Half the time the symptom is a syntax typo.

When to use this vs alternatives

Use firewalld when you need dynamic zone management and seamless NetworkManager integration. Use nftables directly when you are building a custom router or need performance tuning that bypasses the daemon abstraction. Use iptables only when maintaining legacy scripts that predate Fedora 34. Stick to firewall-cmd for desktop and standard server workloads.

Reload the daemon after every persistent change. Future-you will thank you.

Where to go next