On Rocky Linux, AlmaLinux, RHEL and Fedora, SELinux is on by default. It’s the reason Nginx returns 403 for files with perfect Unix permissions, PHP can’t connect to the database, or your app can’t bind to port 8081. The common advice, “just disable SELinux”, throws away a strong security layer. Most SELinux problems take one command to fix properly.
What SELinux does
Normal Linux permissions (owner, group, mode) are discretionary: the owner decides. SELinux adds mandatory rules on top. Every process and file has a label (context), and a policy defines which process types may access which file types, ports and actions.
Example: Nginx runs as httpd_t. It may read files labelled httpd_sys_content_t, but not files in a home directory labelled user_home_t, even if the permissions are 777. If Nginx is compromised, the attacker is still boxed in.
Check the mode
getenforce # Enforcing, Permissive or Disabled
sestatus
- Enforcing — the policy is enforced and violations are blocked and logged.
- Permissive — nothing is blocked, but violations are logged. Use it for troubleshooting.
- Disabled — SELinux is off.
Switch temporarily (until reboot):
sudo setenforce 0 # permissive
sudo setenforce 1 # enforcing
The permanent setting is SELINUX= in /etc/selinux/config.
Quick diagnosis: if setenforce 0 makes the problem disappear, it’s SELinux. Then fix it properly and switch back with setenforce 1.
See file and process labels
ls -Z /var/www/html
# unconfined_u:object_r:httpd_sys_content_t:s0 index.html
ps -eZ | grep nginx
# system_u:system_r:httpd_t:s0 … nginx
The important part is the type (httpd_sys_content_t, httpd_t).
Read the denials
sudo ausearch -m AVC -ts recent
sudo journalctl -t setroubleshoot # if setroubleshoot is installed
Install setroubleshoot-server for human-readable explanations that usually include the exact fix command:
sudo dnf install setroubleshoot-server
sudo sealert -a /var/log/audit/audit.log
The four fixes that solve most problems
1. Wrong file label: restorecon / semanage fcontext
Files moved (mv) from your home directory keep their old label. Files copied get the label of the destination. Restore the default labels:
sudo restorecon -Rv /var/www/html
For a custom web root like /srv/www, first tell SELinux what it is, then apply:
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"
sudo restorecon -Rv /srv/www
Writable directories (uploads, caches) need the read-write type:
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/srv/www/site/uploads(/.*)?"
sudo restorecon -Rv /srv/www/site/uploads
(semanage is in the policycoreutils-python-utils package.)
chcon changes labels too, but a relabel undoes it. Use semanage fcontext for permanent rules.
2. A feature is switched off: booleans
Policies include on/off switches for common needs:
getsebool -a | grep httpd
sudo setsebool -P httpd_can_network_connect on # Nginx/Apache as reverse proxy to an app
sudo setsebool -P httpd_can_network_connect_db on # PHP to a remote database
sudo setsebool -P httpd_can_sendmail on # PHP mail()
sudo setsebool -P httpd_execmem on # some PHP/JIT setups
-P makes it persistent. The classic case: Nginx proxy_pass to 127.0.0.1:3000 fails with 502 and a “Permission denied” in the error log. The fix is httpd_can_network_connect.
3. A non-standard port: semanage port
Running SSH on port 2222, or Nginx on 8081:
sudo semanage port -l | grep -E 'ssh|http_port'
sudo semanage port -a -t ssh_port_t -p tcp 2222
sudo semanage port -a -t http_port_t -p tcp 8081
Without this, the service fails to start with “Permission denied” on bind. See change SSH port.
4. Something truly custom: a local policy module
When no label, boolean or port fixes it, generate a small policy from the denials, after reviewing what it allows:
sudo ausearch -m AVC -ts recent | audit2allow -M mylocal
cat mylocal.te # read what it grants
sudo semodule -i mylocal.pp
Don’t blindly allow everything. That’s the same as disabling SELinux for that process.
Containers
Podman and Docker volumes on SELinux systems need relabelling for container access. Add :Z (private) or :z (shared) to the volume:
podman run -v /srv/data:/data:Z image
Why not just disable it?
Disabling SELinux removes protection that limits the damage of a compromised web app or service, which is exactly the scenario that matters on internet-facing servers. Fixes like the ones above take a minute once you know them. If you must, use permissive mode temporarily while you collect denials, then fix them and re-enable enforcing.
Re-enabling after a full disable requires a relabel of the filesystem at boot (touch /.autorelabel), which takes a while on big disks.
Quick reference
| Task | Command |
|---|---|
| Mode | getenforce / sestatus |
| Temporarily permissive | setenforce 0 |
| Denials | ausearch -m AVC -ts recent |
| Restore labels | restorecon -Rv /path |
| Label custom path | semanage fcontext -a -t TYPE "/path(/.*)?" |
| Booleans | setsebool -P name on |
| Allow port | semanage port -a -t TYPE -p tcp PORT |
| Container volume | -v /host:/ctr:Z |
Related: AppArmor basics and Linux server hardening.