How to Use Firewall and SELinux Together for Defense in Depth on Fedora

Fedora ships with both firewalld and SELinux enabled by default — using them together gives you network-level and process-level protection that significantly reduces your attack surface.

You opened the port, but the service still fails

You deployed a web server on Fedora. You added a rule to allow traffic on port 443. You can curl localhost and get the page. You try to curl from another machine and get a timeout. You check the firewall again, the port is open. You check the logs and see avc: denied. You are stuck between a firewall that says "allow" and a kernel module that says "no".

This is the intersection of firewalld and SELinux. One controls the door. The other controls what happens inside the room.

What's actually happening

Defense in depth means layers. firewalld is the bouncer at the club door. It checks IDs and decides who gets in. If the bouncer stops you, you never touch the dance floor. SELinux is the security guard inside the club. Even if someone slips past the bouncer, the guard stops them from going backstage, stealing the soundboard, or opening the back exit.

firewalld sits in the kernel network stack. It inspects packets before they reach the socket. It filters based on ports, protocols, and source addresses. SELinux sits in the kernel security subsystem. It inspects syscalls made by processes. It confines based on labels and policies.

When a web server receives a request, the packet passes firewalld first. If allowed, the packet reaches the socket. The web server process then reads the request. If the server needs to read a config file, SELinux checks the process label against the file label. If the check fails, the read syscall returns EACCES. The application sees a permission error. The firewall never sees this error. The firewall only knows about the network connection.

This separation is why you can have a working firewall and a broken service. Fedora enables both by default. firewalld manages nftables rules dynamically. SELinux runs in Enforcing mode out of the box. Disabling either reduces security. The goal is to configure them, not disable them.

Reboot before you debug. Half the time the symptom is gone.

Check that both are active

Verify the state of both subsystems. firewalld should report running. SELinux should report Enforcing.

# Check firewalld state. Running means the daemon is active and managing rules.
sudo firewall-cmd --state
# Check SELinux mode. Enforcing means denials are blocked and logged. Permissive means denials are only logged.
getenforce

If getenforce reports Permissive or Disabled, re-enable enforcement. Edit /etc/selinux/config and set SELINUX=enforcing. Files in /etc/ are user-modified. Files in /usr/lib/ ship with packages and should never be edited. Your changes in /etc/ survive updates. Reboot the system after changing the config.

Set SELINUX=enforcing in /etc/selinux/config and reboot.

Add firewall rules for only the services you need

Never open ports you do not use. Use predefined service names where possible. Service names map to ports and protocols defined in XML files. This makes rules readable and maintainable. firewalld maintains two configurations. The runtime configuration is active in memory. The persistent configuration is stored on disk. Commands without --permanent affect runtime only. They vanish on reboot. Commands with --permanent affect disk only. They do not apply until you reload.

# Add SSH permanently. The --permanent flag writes to the config file on disk.
sudo firewall-cmd --permanent --add-service=ssh
# Add HTTPS permanently. Service names abstract port numbers and protocols.
sudo firewall-cmd --permanent --add-service=https
# Reload the firewall. Changes to --permanent rules do not apply until you reload.
sudo firewall-cmd --reload

firewall-cmd --reload is mandatory after every persistent change. The runtime configuration and the persistent configuration diverge if you skip this.

Run firewall-cmd --reload. Changes do not apply until you reload.

Use SELinux booleans to tighten service permissions

SELinux uses booleans to toggle specific behaviors. Booleans are safe to change. They do not require recompiling policy. Use booleans to adjust service permissions without disabling confinement.

# Check if httpd is allowed to make outbound network connections.
getsebool httpd_can_network_connect
# Disable outbound connections for httpd. The -P flag makes the change persistent across reboots.
sudo setsebool -P httpd_can_network_connect off

List booleans for a service to see what is available.

# List all booleans containing httpd. This helps find the right toggle for your use case.
getsebool -a | grep httpd

List booleans before changing them. Blind toggling hides misconfigurations.

Label custom directories correctly

SELinux labels files with contexts. Processes can only access files with allowed contexts. If you store data in a non-standard directory, the directory inherits the wrong label. You must define the correct context and restore it.

# Add a file context rule. This tells SELinux that /srv/mysite and its contents should have the httpd_sys_content_t type.
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/mysite(/.*)?"
# Apply the context rule to the filesystem. This updates the labels on disk based on the rule you just added.
sudo restorecon -Rv /srv/mysite

semanage fcontext adds the rule to the policy database. restorecon applies it. You need both. chcon changes the label on disk immediately. It does not update the policy database. If you run restorecon later, or if the system relabels, chcon changes are lost. Always use semanage fcontext followed by restorecon.

Use semanage fcontext, not chcon. Temporary labels vanish on relabel.

Verify it worked

Verify the firewall allows traffic. Verify SELinux allows the process.

# List all active zones and rules. Check that your services appear in the allowed list.
sudo firewall-cmd --list-all
# Check for recent SELinux denials. An empty output means no denials occurred since the last boot or log rotation.
sudo ausearch -m avc -ts recent

If ausearch returns lines, something is blocked. Read the denial. Do not disable SELinux.

Run ausearch after every change. Silence means success.

Common pitfalls and what the error looks like

You will see avc: denied messages in the audit log. This means SELinux blocked an action. The message includes the source process, the target object, and the operation.

type=AVC msg=audit(1698765432.123:456): avc:  denied  { read } for  pid=1234 comm="httpd" name="index.html" dev="sda1" ino=789 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

The scontext shows the process type (httpd_t). The tcontext shows the file type (user_home_t). The denial is { read }. The web server tried to read a file in a home directory. The fix is to move the file to a web directory or change the context. Never add permissive=1 to the process type as a quick fix. That disables confinement for the whole process.

Use sealert to get a plain-English explanation.

# Install setroubleshoot-server if sealert is missing. This package provides human-readable explanations for denials.
sudo dnf install setroubleshoot-server
# Analyze the audit log. sealert reads the raw AVC messages and suggests safe fixes like booleans or file contexts.
sudo sealert -a /var/log/audit/audit.log

SELinux denials also appear in journalctl -t setroubleshoot. These messages are summarized and easier to read than raw audit logs. When a service fails, check the firewall first. It is faster to verify. If the port is open, check SELinux. If no denials, check journalctl -xeu <service>. The x flag adds explanatory text. The e flag jumps to the end.

Read the denial. Fix the policy. Never disable SELinux to make the error go away.

When to use this vs alternatives

Use firewalld when you need to control network access based on ports, protocols, and source addresses. Use SELinux booleans when you need to toggle specific service behaviors like allowing outbound connections or accessing user home directories. Use semanage fcontext when you store data in non-default directories and need to assign the correct security label. Use sealert when you encounter an AVC denial and need a plain-English explanation of the fix. Use audit2allow only when you are writing custom policy modules for applications that lack a standard SELinux policy. Stay on Enforcing mode for production systems. Switch to Permissive mode only during initial debugging when you cannot resolve a denial immediately.

Where to go next