The firewall confusion starts at the command line
You install a web server on your Fedora machine and try to reach it from another device on the network. The connection times out. You search for a solution and find three different sets of commands. One uses iptables -A INPUT. Another uses nft add rule. A third uses firewall-cmd --add-service. You run the iptables command, and it works for five minutes. You reboot, and the rule is gone. You try to run firewall-cmd and it complains about conflicting configurations. The terminal output gives you three different names for what feels like the same problem.
The confusion comes from looking at three different abstraction layers as if they are competing products. They are not. They are stacked components that handle network packet filtering at different levels of the system. Fedora ships with all three because the kernel, the legacy compatibility layer, and the user-space management daemon each serve a distinct purpose. Understanding which layer you are talking to stops the guesswork and keeps your network rules stable across reboots and updates.
Pick the right layer for your task. Leave the others alone.
What is actually happening under the hood
The Linux kernel filters network traffic using a subsystem called netfilter. For over a decade, the user-space tool that talked to netfilter was iptables. The kernel modules and the command-line syntax grew complex. The data structures were fragmented. Performance suffered when rulesets grew large. The kernel developers rewrote the filtering engine from scratch and named it nftables. The new engine uses a unified bytecode compiler, shared sets, and a cleaner API. It replaced the old kernel modules entirely.
The iptables command did not disappear. Fedora ships iptables-nft, which is a translation shim. When you type iptables -L, the shim converts the legacy syntax into nftables bytecode and sends it to the kernel. The output looks familiar, but the underlying mechanism is completely different. The shim exists so old scripts and tutorials do not break overnight. It is a compatibility bridge, not a modern toolchain.
firewalld sits above both of them. It is a daemon that manages firewall rules using a zone-based model. Instead of writing raw packet filters, you assign network interfaces to zones like public, home, or trusted. Each zone has a predefined set of allowed services and ports. firewalld translates your zone assignments into nftables rules and pushes them to the kernel. It handles runtime updates, persistent storage, and dynamic interface changes without requiring you to understand packet matching syntax.
Think of the stack like a vehicle. nftables is the engine and transmission. iptables-nft is an adapter that lets you use an older steering wheel. firewalld is the dashboard with preset driving modes. You do not need to rebuild the engine to change lanes. You adjust the dashboard controls and let the system handle the mechanics.
Keep the layers separate. Mixing direct kernel rules with a management daemon creates silent conflicts that break on the next reload.
How to manage your firewall without breaking things
Fedora enables firewalld by default on Workstation and Server editions. The daemon maintains two separate configurations. The runtime configuration lives in memory and applies immediately. The persistent configuration lives on disk and survives reboots. Changes made without the --permanent flag vanish when the daemon restarts. Changes made with --permanent do not apply until you reload the daemon. This split prevents accidental lockouts during live configuration.
Here is how to check the current state and verify which zone your active interface belongs to.
sudo firewall-cmd --state
# Confirms the daemon is running and accepting commands
sudo firewall-cmd --get-active-zones
# Maps interfaces to their assigned zones
# Shows which ruleset is currently protecting each connection
Adding a service requires two steps. First, you declare the change in the persistent configuration. Second, you apply it to the live system. The daemon refuses to mix the two in a single command to force explicit intent.
sudo firewall-cmd --permanent --add-service=http
# Writes the rule to the disk configuration file
# Does not touch the live kernel ruleset yet
sudo firewall-cmd --reload
# Flushes the runtime ruleset and rebuilds it from disk
# Applies the new service to the active zones immediately
Opening a raw port follows the same pattern. You specify the port number and protocol, mark it permanent, then reload.
sudo firewall-cmd --permanent --add-port=8080/tcp
# Targets the persistent configuration only
# Uses tcp to match the transport layer protocol
sudo firewall-cmd --reload
# Rebuilds the nftables ruleset from the updated disk state
# Activates the port without dropping existing connections
When you need conditional logic, such as allowing traffic only from a specific subnet or rate-limiting connections, you use rich rules. Rich rules are XML-like strings that firewalld parses and compiles into nftables expressions. They give you granular control without leaving the daemon.
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/24" service name="ssh" accept'
# Restricts SSH access to the specified subnet
# Uses the rich rule parser for conditional matching
sudo firewall-cmd --reload
# Compiles the rich rule into nftables bytecode
# Applies the subnet restriction to the live firewall
Reload the daemon after every persistent change. The runtime and persistent configurations will drift the moment you skip this step.
Verify the rules are active
Verification requires checking both the management layer and the kernel layer. firewalld provides a human-readable view of what it believes is active. nft shows what the kernel is actually enforcing. Both views must align.
Here is how to inspect the active zone configuration and confirm your service is listed.
sudo firewall-cmd --zone=public --list-all
# Dumps the runtime rules for the specified zone
# Shows interfaces, sources, services, ports, and rich rules
sudo firewall-cmd --zone=public --list-services
# Filters the output to show only allowed service names
# Confirms http or ssh appears in the active set
If you want to see the compiled kernel rules, query the nftables ruleset directly. firewalld stores its rules in the filter and nat tables under the inet family. The output will look different from iptables output, but the logic matches your zone assignments.
sudo nft list ruleset | grep -A 5 "table inet filter"
# Queries the kernel ruleset directly
# Filters for the inet filter table where firewalld stores rules
# Shows the compiled nftables expressions matching your zones
Run systemctl status firewalld before you restart the daemon. The status output shows recent log lines and confirms the service is active. Guessing why a rule failed without checking the daemon state wastes time.
Common pitfalls and what the errors look like
The most frequent mistake is forgetting to reload after a persistent change. The command returns success, but the kernel never sees the new rule. You test the connection and it fails. The error you see is usually a timeout or a connection refused message from the remote host, not from the firewall itself. The firewall is silently ignoring traffic because the runtime ruleset still reflects the old configuration.
Another common trap is mixing iptables commands with firewalld. When firewalld is running, it owns the nftables ruleset. If you run iptables -A INPUT -j DROP, the iptables-nft shim injects a rule into the same table. firewalld does not track that rule. The next time you run firewall-cmd --reload, the daemon flushes the entire table and rebuilds it from its own configuration. Your manual iptables rule disappears. You will see this warning in the journal if you try to run both managers simultaneously.
Warning: ALREADY_ENABLED: 'public': 'ssh'
That warning means the service is already in the runtime configuration. It is harmless. The dangerous scenario is the silent flush. If you see rules vanish after a reload, check whether you injected commands outside of firewall-cmd.
SELinux also interacts with the firewall. If you change a service port to a non-standard number, the firewall may allow the traffic, but SELinux will block the daemon from binding to the port. The firewall logs will show the packet passing through. The application logs will show a permission denied error. You must adjust the SELinux port label alongside the firewall rule. The two systems enforce different boundaries. One controls network packets. The other controls process access.
Check the runtime configuration before you reboot. A mismatch between disk and memory will force you to troubleshoot over SSH or console access.
Which tool matches your workflow
Use firewalld when you want a stable, zone-based firewall that survives reboots and handles dynamic interface changes automatically. Use nftables directly when you need custom maps, complex stateful tracking, or performance tuning that exceeds the rich rule syntax. Use iptables-nft only when you are maintaining legacy scripts that cannot be rewritten for the new syntax. Stay on the default firewalld stack if you are running standard services and do not need packet-level bytecode optimization.
Trust the package manager and the daemon. Manual rule injection drifts, snapshots stay.