After years on Ubuntu, I switched my daily-driver workstation to Fedora (originally Fedora 36, around the time this post first went up). The piece I want to focus on here is firewall management — specifically, moving off of UFW and learning to drive firewalld properly on a host that runs a lot of virtual machines for security research.

firewalld provides a dynamically managed firewall with support for network/firewall zones that define the trust level of network connections or interfaces. It supports IPv4, IPv6, Ethernet bridges, and ipsets, and it cleanly separates runtime and permanent configuration. — firewalld.org

The zone model is the important idea. Each network interface (or source) is bound to a zone, and the zone determines which services and ports are reachable on that interface. This is a much better mental model than the flat ruleset I was used to with UFW or raw iptables.

Environment

The host runs Fedora with QEMU/KVM managed through virt-manager (the libvirt stack). The default install puts the primary NIC into the FedoraWorkstation zone and the virtual bridge into the libvirt zone. Out of the box FedoraWorkstation permits dhcpv6-client, mdns, samba-client, and ssh — note that permitted does not mean a service is running. The OpenSSH server is not enabled by default on Fedora Workstation, so port 22 only matters once you start sshd.service. Worth checking either way.

The use case that drove me to actually learn this: I wanted to serve files from the host to my VMs over an ad-hoc HTTP server (e.g. python3 -m http.server 8000) without exposing that port to anything else.

Useful commands

Run as root or with sudo.

List active zones (which zones currently have an interface or source bound):

firewall-cmd --get-active-zones

List everything allowed in a zone:

firewall-cmd --zone=FedoraWorkstation --list-all
firewall-cmd --zone=libvirt --list-all

Remove a service you don’t need (note the hyphen — it’s --remove-service, not --remove service):

firewall-cmd --zone=FedoraWorkstation --remove-service=dhcpv6-client
firewall-cmd --zone=FedoraWorkstation --remove-service=samba-client
firewall-cmd --zone=FedoraWorkstation --remove-service=mdns

The inverse uses --add-service=<name>. To see the full catalog of named services: firewall-cmd --get-services.

Letting specific VMs reach the host

Rather than open a port to the world, bind your VM’s address (or subnet) into a trusted zone as a source. Anything from that source will be evaluated against that zone’s rules:

firewall-cmd --zone=trusted --add-source=192.168.122.50

Now 192.168.122.50 can hit any open port on the host. Repeat per VM, or use a CIDR (192.168.122.0/24) for the whole virtual network. Confirm:

firewall-cmd --get-active-zones
firewall-cmd --zone=trusted --list-all

Runtime vs. permanent

This trips a lot of people up. Every command above modifies the runtime config — gone on reboot or systemctl reload firewalld. There are two ways to make changes stick:

  1. Stage in runtime, verify, then commit:
    firewall-cmd --runtime-to-permanent
    
  2. Or write directly to permanent and reload:
    firewall-cmd --permanent --zone=FedoraWorkstation --remove-service=samba-client
    firewall-cmd --reload
    

I prefer option 1 for anything non-trivial — you get to confirm the rule works before locking it in.

Sanity check after a reload

systemctl reload firewalld
firewall-cmd --get-active-zones
firewall-cmd --zone=FedoraWorkstation --list-all
firewall-cmd --zone=libvirt --list-all

Conclusion

firewalld is more verbose than UFW, but the zone model fits the way a research host actually works: a hostile-internet-facing zone, a trusted-VM-facing zone, and explicit rules about what crosses between them. Once you internalize runtime vs. permanent and the difference between adding a service and adding a source, day-to-day management is straightforward.