RCWRCW IT TrainingFree hands-on labs & simulators← Back to home
Linux · Detailed troubleshooting guide

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.

Published October 6, 2026 · By , Enterprise Infrastructure Architect

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:

BooleanAllows
httpd_can_network_connectWeb server to make outbound TCP connections (reverse proxy, API calls)
httpd_can_network_connect_dbWeb server to reach a database port specifically
httpd_use_nfs / httpd_use_fusefsServing content from NFS or FUSE mounts
nis_enabledBroad outbound RPC, often needed by legacy auth
samba_export_all_rwSamba 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

  1. Confirm SELinux is implicated with setenforce 0, then put it straight back.
  2. Read the AVC: source context, target context, permission, class.
  3. Run audit2why — it will tell you outright if a boolean covers it.
  4. Boolean? setsebool -P. Done.
  5. Wrong label on a file? restorecon, or semanage fcontext -a then restorecon for custom paths.
  6. Non-standard port? semanage port -a or -m.
  7. Nothing logged at all? semodule -DB, reproduce, then semodule -B.
  8. 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.

Key takeaway: An AVC tells you the source context, the target context and the permission in one line — read those three and the fix follows. Reach for a boolean first, a file context second, a port label third, and a custom module only when none of those fit. setenforce 0 is a diagnostic step, never a remediation.