The Trap Almost Every Developer Falls Into
You just finished deploying a web app to Fedora Server. Nginx is running, the firewall has port 80 open, and the config files look perfectly fine. Yet when you try to access it… 502 Bad Gateway. You check the Nginx logs and see connect() to unix:/run/myapp.sock failed (13: Permission denied). You run ls -la, permissions are 755 — you even try chmod 777 just to be sure — still nothing.
The real culprit? SELinux — the thing that 90% of new Fedora/RHEL users immediately want to disable the moment they encounter it.
I’ve been using Fedora as my primary development machine for two years. Fast package updates, a constantly evolving kernel — I love that. But SELinux is still the thing I “fight” the most every time I set up a new environment. This article is a collection of everything I’ve learned after getting blocked by it one too many times.
Why Does SELinux Work the Way It Does?
Traditional Linux access control uses the DAC (Discretionary Access Control) model — based on user/group/permission bits. If you own the file, you can do whatever you want with it.
SELinux adds a MAC (Mandatory Access Control) layer on top of that. The key difference: it doesn’t care whether you’re root or not — it cares about the security context (security label) attached to each process and file.
Every file, process, and port carries a context in the format:
user:role:type:level
Run ls -Z to see the actual context:
ls -Z /var/www/html/
# Output:
system_u:object_r:httpd_sys_content_t:s0 index.html
The Nginx process runs with type httpd_t. It’s only allowed to read files with type httpd_sys_content_t. When you copy a file from /home into /var/www/html, that file keeps its original context — usually user_home_t or tmp_t. Nginx gets denied even if the permission bits are 777. This is the most common cause of SELinux permission denied errors when deploying a web app.
Three Ways to Solve It — From Worst to Best
Option 1: Disable SELinux (don’t do this in production)
Google “SELinux permission denied” and the first answer you’ll usually find is:
setenforce 0
# Or worse — disable it completely:
sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
Quick. Immediately effective. And completely wrong.
Disabling SELinux is like silencing a fire alarm because it’s too loud. Fedora and all RHEL-based distros enable SELinux by default because it prevents privilege escalation attacks — where an attacker exploits a vulnerability in Nginx or PHP to break out of the process. Disabling it removes that protection layer entirely.
Option 2: Permissive Mode for Debugging
Permissive mode allows everything to run but still logs violations — very useful for understanding what permissions your app needs before writing a policy:
# Switch to Permissive temporarily (resets after reboot)
setenforce 0
# Check current mode
getenforce
# Output: Permissive or Enforcing
# View denial logs
ausearch -m avc -ts recent
# Or monitor in real time:
tail -f /var/log/audit/audit.log | grep denied
Only use Permissive mode during the debugging phase. Leaving it on a production server is accepting unnecessary security risk.
Option 3: Fix the Context Properly — Do This First
When a file has the wrong context — the most common case being copying from the home directory into /var/www:
# View the current context of the file
ls -Z /var/www/html/myapp/
# Restore to the default context for that directory
restorecon -Rv /var/www/html/myapp/
# Or set it manually
chcon -R -t httpd_sys_content_t /var/www/html/myapp/
Common pitfall: chcon only makes temporary changes — they get reset when SELinux relabels the entire filesystem (for example, after touch /.autorelabel followed by a reboot). For persistent changes, use semanage fcontext:
# Add a persistent rule for directories outside /var/www
semanage fcontext -a -t httpd_sys_content_t "/srv/myapp(/.*)?"
# Apply the newly added rule
restorecon -Rv /srv/myapp/
Creating a Custom Policy for Complex Applications
Most tutorials skip this section — because it requires understanding exactly what your application needs to do. But this is precisely what you need when deploying a real-world application: connecting to non-standard ports, writing to directories outside the usual conventions, or running background jobs with complex system calls.
Step 1: Collect Sufficient Denial Logs
Switch to Permissive mode, run your app, and test every feature. This is critical: test everything — file uploads, API calls, cron jobs — so SELinux logs all the permissions being blocked. Miss one feature and you’ll get blocked again after loading the policy.
setenforce 0
# Run the app and test all features...
ausearch -m avc -ts today > /tmp/denials.log
Step 2: Generate a Policy with audit2allow
# Install tools if not already present
dnf install -y policycoreutils-python-utils
# Generate a policy module from the denial log
ausearch -m avc -ts today | audit2allow -M myapp_policy
# This creates 2 files:
# myapp_policy.te — Type Enforcement source (human-readable)
# myapp_policy.pp — Compiled Policy Package
Read through the .te file before doing anything else:
cat myapp_policy.te
# Example output:
# allow httpd_t unreserved_port_t:tcp_socket name_connect;
# allow httpd_t var_t:file { read write create };
The second line — allow httpd_t var_t:file { read write create } — is a red flag. The type var_t is too broad, covering /var/lib and /var/log for the entire system. You should narrow it down to a more specific type and recompile manually using checkmodule + semodule_package rather than loading it directly.
Step 3: Load the Policy and Re-enable Enforcing
semodule -i myapp_policy.pp
setenforce 1
# Verify it was loaded
semodule -l | grep myapp
Allowing Your App to Bind to a Custom Port
App trying to bind to port 8080 and getting blocked? SELinux manages network ports too — each port is assigned a specific type, and only processes with the matching type can bind to it:
# See which ports are assigned to which types
semanage port -l | grep http
# Add port 8080 to http_port_t
semanage port -a -t http_port_t -p tcp 8080
# Confirm
semanage port -l | grep 8080
Booleans — Toggle Features Without Writing a Policy
Fedora ships with over 300 boolean switches for common scenarios. Before sitting down to write a policy from scratch, check the booleans first — there’s often one that does exactly what you need:
# View booleans related to httpd/nginx
getsebool -a | grep httpd
# Allow nginx to connect outbound (e.g., proxying to a backend)
setsebool -P httpd_can_network_connect on
# Allow httpd to read home directories
setsebool -P httpd_enable_homedirs on
# -P to persist after reboot
Debugging Workflow for SELinux Issues
The workflow I typically follow, from simple to complex:
- Run
sestatus— confirm you’re in Enforcing mode - Run
ausearch -m avc -ts recent— find the most recent denial and identify which type is being blocked - Try the simple fixes first:
restoreconif it’s a context issue, a boolean if it’s a common scenario,semanage portif it’s a port issue - If that’s not enough → Permissive mode → collect all denials →
audit2allow - Carefully read the .te file and narrow down overly broad permissions before compiling
- Load the policy, test all features, re-enable Enforcing
Quick Reference Commands
# View the context of running processes
ps auxZ | grep nginx
# View all mapped ports
semanage port -l
# Find denials by a specific process name
ausearch -m avc -c nginx
# List loaded custom policies
semodule -l | grep -v ^base
The first time I truly appreciated SELinux was when a vulnerability in a Node.js app was exploited — the attacker had shell access inside the process, but couldn’t break out because SELinux blocked every system call outside the policy. The server kept running normally; only the audit log showed dozens of denial attempts. After that, I stopped seeing SELinux as something that gets in the way.

