SELinux Denials: Reading AVCs, Fixing Contexts and Knowing When Not to Disable It
Setting SELinux to permissive is the fastest way to end an outage and the fastest way to fail an audit. This guide reads denials the way the policy engine does, so you can fix the actual label, boolean or port in about the same time it takes to disable enforcement.
Confirm SELinux is actually the cause
Thirty seconds of checking prevents hours of chasing the wrong subsystem. SELinux denials are silent to the application: you usually see "permission denied" with filesystem permissions that look perfectly correct.
getenforce # Enforcing | Permissive | Disabled sestatus # mode, policy, and whether config matches runtime ausearch -m AVC,USER_AVC -ts recent # denials in the last few minutes
The decisive test is to toggle enforcement briefly and see whether the symptom disappears:
setenforce 0 # reproduce the failure setenforce 1
If the behaviour is unchanged, SELinux is not involved — stop here and look at ownership, ACLs or the application. If it is involved, the denial you just generated in permissive mode is now logged, which is precisely the evidence you need.
Decode the AVC line
Every denial carries the same five fields. Once you can read them in order, most denials explain themselves:
type=AVC msg=audit(1759740000.123:456): avc: denied { name_bind } for
pid=2841 comm="nginx" src=8443
scontext=system_u:system_r:httpd_t:s0
tcontext=system_u:object_r:unreserved_port_t:s0
tclass=tcp_socket permissive=0
- { name_bind } — the permission that was refused. This names the operation.
- comm / pid — who tried it.
- scontext — the source context: the domain the process runs in (
httpd_t). - tcontext — the target context: the label on the thing being touched (
unreserved_port_t). - tclass — the kind of object:
file,dir,tcp_socket,process.
Read it as a sentence: the httpd_t domain was denied name_bind on a tcp_socket labelled unreserved_port_t. That is a port-labelling problem, and no amount of chmod will touch it.
Turn raw audit records into readable explanations:
ausearch -m AVC -ts today | audit2why ausearch -m AVC -ts recent -i # -i interprets numeric ids journalctl -t setroubleshoot --since today
audit2why is the most under-used tool in the set: it states whether a boolean would allow the access, which turns a policy puzzle into a one-line fix.
Booleans: the fix that is already written for you
Distribution policy ships with tunable switches for the things administrators commonly need. If a boolean covers your case, use it — it is supported, survives updates, and needs no custom module.
getsebool -a | grep httpd # all httpd-related switches semanage boolean -l | grep -i nfs # with descriptions and defaults
Common ones worth knowing by name:
| Boolean | Allows |
|---|---|
httpd_can_network_connect | Web server to make outbound TCP connections (reverse proxy, API calls) |
httpd_can_network_connect_db | Web server to reach a database port specifically |
httpd_use_nfs / httpd_use_fusefs | Serving content from NFS or FUSE mounts |
nis_enabled | Broad outbound RPC, often needed by legacy auth |
samba_export_all_rw | Samba to export any path read-write |
Set it persistently with -P. Without that flag the change is lost at reboot, producing a fault that reappears weeks later with no apparent trigger:
setsebool -P httpd_can_network_connect on
File contexts: the problem is almost always a moved file
A file carries its label with it when moved with mv, and inherits the destination's label when copied with cp. This single asymmetry explains most "it worked in my home directory" failures:
ls -Z /var/www/html/index.html # unconfined_u:object_r:user_home_t:s0 ← wrong, came from a mv matchpathcon /var/www/html/index.html # /var/www/html/index.html system_u:object_r:httpd_sys_content_t:s0 ← expected
When the actual and expected labels differ, restore from policy rather than setting a label by hand:
restorecon -Rv /var/www/html
chcon also works but is not durable — a filesystem relabel or restorecon run will revert it. Treat chcon as a test and restorecon as the fix.
For a non-standard path, register the rule in policy first, then apply it. This is what makes the label survive relabels and reboots:
semanage fcontext -a -t httpd_sys_content_t "/srv/sites(/.*)?" restorecon -Rv /srv/sites semanage fcontext -l | grep '/srv/sites' # verify the rule is recorded
Note the regex form (/.*)? — it matches the directory itself and everything beneath it. Omitting it labels the directory but not its contents.
Ports, and the denial that has nothing to do with files
SELinux labels TCP and UDP port numbers as well as files. Running a service on a non-default port fails at bind() even when the firewall is open and the process is root:
semanage port -l | grep http_port_t # http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000 semanage port -a -t http_port_t -p tcp 8443 # add a new port semanage port -m -t http_port_t -p tcp 9443 # modify one already owned by another type
Use -a for a port no type currently claims and -m when another type already owns it; using the wrong one returns "port already defined", which reads like a conflict but is only a verb mismatch.
dontaudit: the denials you are not being shown
Policy suppresses a large class of harmless denials to keep logs readable. When a failure is real but nothing appears in the audit log, those suppressions are the likely reason. Disable them temporarily:
semodule -DB # disable dontaudit, rebuild policy # reproduce the failure ausearch -m AVC -ts recent semodule -B # restore dontaudit rules
Always re-enable with semodule -B when finished. Leaving suppressions off floods the audit log and can itself cause disk-pressure incidents on busy hosts.
Writing a module — carefully
When no boolean, context or port applies, generate a module. The danger is that audit2allow will happily grant everything it sees, including denials unrelated to your problem, so filter the input tightly:
# scope to one process and one time window, review before building ausearch -m AVC -ts recent -c nginx | audit2allow -m nginx_local # read the generated rules. only then build and install ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local semodule -i nginx_local.pp
Read the .te output before installing. Rules granting broad permissions on process, or anything mentioning unconfined_t, indicate the input was too wide — narrow the ausearch filter and regenerate.
semodule -l | grep nginx_local # confirm it loaded semodule -r nginx_local # remove if it was wrong
If you must relabel an entire filesystem after a long spell with SELinux disabled, schedule it — on a large filesystem it is slow and the machine is unusable while it runs:
touch /.autorelabel && systemctl reboot
Decision order
- Confirm SELinux is implicated with
setenforce 0, then put it straight back. - Read the AVC: source context, target context, permission, class.
- Run
audit2why— it will tell you outright if a boolean covers it. - Boolean?
setsebool -P. Done. - Wrong label on a file?
restorecon, orsemanage fcontext -athenrestoreconfor custom paths. - Non-standard port?
semanage port -aor-m. - Nothing logged at all?
semodule -DB, reproduce, thensemodule -B. - Only then build a narrow module with
audit2allow, and read the rules before installing.
Permanently disabling SELinux in /etc/selinux/config should be a documented, risk-accepted decision — not the default response to a denial you have not read.
setenforce 0 is a diagnostic step, never a remediation.