What This Chapter Solves
One class of failure is genuinely baffling: file modes, ownership and paths are all correct, namei -l walks cleanly, and the service is still denied — with nothing in the log but Permission denied. On RHEL/CentOS/Rocky that usually means SELinux.
It does not ask “who are you”. It asks “what label does this process carry, and what label does the object it wants carry?” That is an entirely different model.
1. Three Modes (First, Find Out Whether It Is Even Involved)
getenforce # Enforcing / Permissive / Disabled
sestatus # full status: policy type, policy name, current mode
sestatus -b | head # boolean overview
sudo setenforce 0 # switch to permissive temporarily (gone after reboot; for testing)| Mode | Behaviour |
|---|---|
enforcing | Actually blocks and logs AVC denials |
permissive | Does not block, but still logs AVC — the golden state for diagnosis |
disabled | Policy not loaded at all |
Step one is always “switch to permissive and try to reproduce”. If the problem disappears, it is 100% SELinux. If it persists, go back to permissions and paths instead of burning time on SELinux.
setenforce 0 is a test, never a permanent fix.
2. Security Contexts: What ls -Z Is Telling You
ls -Z /var/www/html/index.html # file context: user:role:type:level
ps -efZ | grep nginx | head # process context — the label the process carries
id -Z # this shell’s context
matchpathcon /var/www/html/index.html # what the policy says this path *should* be ⭐system_u:object_r:httpd_sys_content_t:s0 /var/www/html/index.html
↑ the type — the field you actually work with dailyThe third field (type) is what matters in practice:
| Type | Typically means |
|---|---|
httpd_sys_content_t | content httpd is allowed to read |
httpd_sys_rw_content_t | content httpd may read and write (upload directories) |
var_log_t | log directories |
ssh_port_t / http_port_t | ports a service is allowed to listen on |
unconfined_t | unrestricted (the default for ordinary users) |
Note: matchpathcon answers “what type should this path have?” — the fastest way to detect a label that has been polluted.
3. Fixing Labels: chcon Is Temporary, restorecon Restores, semanage fcontext Is The Real Fix
# ① temporary (lost after relabel; only for verifying a hypothesis)
sudo chcon -t httpd_sys_content_t /srv/www/index.html
# ② restore to what the policy defines (the most common action)
sudo restorecon -Rv /srv/www # -R recursive, -v show what changed
# ③ ⭐ the correct fix: record the rule in policy, then restore
sudo semanage fcontext -a -t httpd_sys_content_t '/srv/www(/.*)?'
sudo restorecon -Rv /srv/www
sudo semanage fcontext -l | grep '/srv/www'Why chcon is not enough: it stamps a one-off patch. The next full relabel (touch /.autorelabel then reboot, or restorecon -R /) resets every hand-changed label back to the policy default — and your service breaks again. semanage fcontext writes the rule into the policy database, which is the durable answer.
4. Booleans: Switches The Policy Already Provides
Many “SELinux will not allow that” situations have an official switch that is simply off by default:
getsebool -a | grep -i httpd | head # httpd-related switches
sudo setsebool -P httpd_can_network_connect on # ⭐ -P makes it persistent
getsebool httpd_can_network_connect
# other frequent ones:
# httpd_can_sendmail allow httpd to send mail
# httpd_read_user_content allow reading home-directory content (site in $HOME)
# container_manage_cgroup container-relatedNote: setsebool without -P is temporary only. I learned this the hard way setting up a reverse proxy: it was on on the test box, and after a reboot it was back to off and the backend was unreachable.
5. Ports And Network: semanage port
Running a service on a non-standard port gets blocked because the policy does not declare that port:
sudo semanage port -l | grep -w http_port_t # which ports http may use
sudo semanage port -a -t http_port_t -p tcp 8080 # add
sudo semanage port -d -t http_port_t -p tcp 8080 # remove6. The Standard Procedure (Follow This Order)
# 1) is it really SELinux? switch to permissive and retry
sudo setenforce 0 # problem gone → it is SELinux; remember setenforce 1 afterwards
# 2) find the AVC denials
sudo ausearch -m avc -ts recent # most recent denials
sudo ausearch -m avc -ts today | tail -20 # today
sudo journalctl -t audit | grep -i denied | tail -10
# 3) human-readable advice (needs setroubleshoot installed)
sudo sealert -a /var/log/audit/audit.log | head -40
# 4) compare labels
ls -Z the-file | head -1; matchpathcon the-file
# 5) fix by kind: label → semanage fcontext + restorecon; switch → setsebool -P; port → semanage port -aA real AVC line looks like this (watch scontext, tcontext, tclass):
type=AVC msg=audit(…): avc: denied { read } for pid=1234 comm="nginx"
name="index.html" scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:user_home_t:s0 tclass=fileHow to read it: scontext is the process (httpd_t), tcontext is the label of the object it wants (user_home_t). The story here is utterly typical: the site files live in a home directory, labelled user_home_t, which httpd may not read. The fix is semanage fcontext + restorecon, or enabling httpd_read_user_content.
7. Relationship To AppArmor (Ubuntu Readers, This Is For You)
| SELinux | AppArmor | |
|---|---|---|
| Model | labels (not paths — types) | paths (per program and path) |
| Default on | RHEL / CentOS / Rocky / Fedora | Ubuntu / Debian / SUSE |
| Status command | getenforce, sestatus | aa-status |
| Config | /etc/selinux/ | /etc/apparmor.d/ |
Note: “there is no SELinux on Ubuntu” is normal — Ubuntu ships AppArmor. If you hit Permission denied on Ubuntu and SELinux is nowhere to be found, check aa-status before blaming anything else.
8. Hands-On Checklist
getenforce; sestatus | head -8 # 1. mode and policy
ls -Z /var/www/html/ | head # 2. file labels
ps -efZ | grep -E 'nginx|httpd' | head # 3. process labels
sudo ausearch -m avc -ts today | tail -20 # 4. today’s AVCs
getsebool -a | grep -i httpd | head # 5. booleans
sudo semanage port -l | grep -w http_port_t # 6. port typesSummary
- SELinux checks labels, not traditional permissions;
enforcingblocks,permissiveonly logs — so step one is “switch to permissive and retry”. - The field you work with is the type in the context;
matchpathcontells you what the policy expects. chconis temporary (a relabel undoes it); the durable fix is alwayssemanage fcontext+restorecon.- Booleans need
setsebool -Pto persist; custom ports needsemanage port -a. - Procedure: permissive test →
ausearch -m avc→sealertadvice → fix label, boolean or port. - Ubuntu-family systems default to AppArmor — do not force SELinux thinking onto them.
Next Up
Firewalls: how netfilter relates to iptables and nftables, the firewalld zone model, ufw in practice, why Docker’s DOCKER-USER chain makes published ports “bypass ufw”, and the checking order when the host allows a port but the outside still cannot reach it (cloud security groups sit further out).
If you enjoyed this, leave a comment~