SELinux denials are dense, cryptic, and almost always fixable in one line β once you know which line. That makes them a tempting thing to paste into an LLM, and the model will happily decode the fields for you. Decoding is the part it does well.
The part it does badly is the fix. Ask a model why nginx gets Permission denied on a RHEL box and a large share of answers start with setenforce 0 or a blanket audit2allow module. Both make the error go away. Both are wrong. This guide covers finding the denial, reading it, and a fix order that keeps the policy intact β plus a prompt that forces the model to follow it.
Step One: Confirm It Is Actually SELinux
A surprising number of "SELinux problems" are ordinary Unix permissions. Rule that out first, then pull the denials from the audit log:
# Is it enforcing at all?
$ getenforce
Enforcing
$ sestatus | grep -E 'mode|policy name'
# Denials from the last 10 minutes, with numeric fields decoded
$ ausearch -m AVC,USER_AVC -ts recent -i
# Everything today for one program
$ ausearch -m AVC -ts today -c nginx -i
# If setroubleshoot is installed, it has already written explanations
$ journalctl -t setroubleshoot --since "1 hour ago" --no-pager
$ sealert -a /var/log/audit/audit.log
-ts recent β means the last ten minutes. Reproduce the failure, then run the search immediately, so you are looking at the denial you caused rather than last week's noise.
USER_AVC matters β denials raised by userspace object managers such as systemd or dbus are logged as USER_AVC, not AVC. Searching for AVC alone misses them.
sealert is a good first read β it ships in setroubleshoot-server, groups identical denials, and often suggests the exact boolean or label. Treat its suggestions as hypotheses, the same as a model's.
No denial, no SELinux problem β if ausearch returns <no matches> and dontaudit rules are not hiding anything (see below), check ls -l, ownership, and the unit's sandboxing options before blaming SELinux.
Anatomy of an AVC Line
Every AVC denial answers the same question: which process, running in which domain, tried to do what to which object carrying which label. Learn to read one yourself before asking a model about it:
type=AVC msg=audit(1727862015.412:1893): avc: denied { read } for pid=24117
comm="nginx" name="index.html" dev="dm-0" ino=3407955
scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:user_home_t:s0
tclass=file permissive=0
| Field | Meaning | In this example |
|---|---|---|
{ read } | The permission that was denied | Reading file contents |
comm / pid | The process that asked | nginx |
scontext | Source context β the domain the process runs in | httpd_t, the web server domain |
tcontext | Target context β the label on the object | user_home_t, a home directory label |
tclass | Object class: file, dir, tcp_socket, process... | A regular file |
permissive | 0 = blocked; 1 = logged but allowed | Actually blocked |
Read that way, the diagnosis is almost obvious: a web server is reading a file labelled as home-directory content. Nobody needs to grant httpd_t access to user_home_t; the file was almost certainly created in someone's home and moved with mv, which preserves the original label. The fix is the label, not the policy.
permissive=1 is not a fix β it means the denial was recorded but the access went through, because the domain or the whole system is permissive. Those lines are useful for debugging and must not be mistaken for a working policy.
Cross-check with audit2why β ausearch -m AVC -ts recent | audit2why states whether a denial is fixable by a boolean or is a missing allow rule. It reads the loaded policy, so its answer is grounded in a way a model's is not.
When Denials Are Hidden: dontaudit
The policy contains dontaudit rules that silence denials considered harmless noise. Occasionally the noise is exactly what is breaking your application, and the log shows nothing at all. Temporarily disable them:
# Rebuild policy with dontaudit rules disabled β denials now log
$ semodule -DB
# Reproduce the failure, then look again
$ ausearch -m AVC,USER_AVC -ts recent -i
# Put the dontaudit rules back as soon as you are done
$ semodule -B
Expect a flood β with dontaudit disabled, the log fills with denials that are normal and harmless. Filter by comm or by the time you reproduced, and do not try to allow everything you see.
Always re-enable β forgetting semodule -B leaves the audit log noisy indefinitely, which buries the next real denial. Put it in your notes as a checklist step.
The Fix Order That Keeps Policy Intact
SELinux problems have a strict hierarchy of fixes, from "the system was misconfigured" to "the policy genuinely lacks a rule". Work down it in order:
# 1. WRONG LABEL β by far the most common cause
$ ls -Z /srv/www/index.html
$ matchpathcon /srv/www/index.html # what the label SHOULD be
$ restorecon -Rv /srv/www # reset to policy defaults
# Non-standard path? Teach the policy, then restore
$ semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"
$ restorecon -Rv /srv/www
# 2. A BOOLEAN β the policy already has the rule, switched off
$ getsebool -a | grep httpd
$ semanage boolean -l | grep httpd_can_network_connect
$ setsebool -P httpd_can_network_connect on
# 3. A PORT β the service binds somewhere non-standard
$ semanage port -l | grep http_port_t
$ semanage port -a -t http_port_t -p tcp 8081
# 4. LAST RESORT β a local policy module
$ ausearch -m AVC -ts recent -c myapp | audit2allow -M myapp_local
$ cat myapp_local.te # READ IT
$ semodule -i myapp_local.pp
Labels: restorecon before chcon β chcon changes a label temporarily β the next relabel or restorecon reverts it. semanage fcontext records the rule in policy so it survives.
Booleans: -P or it does not stick β without -P, setsebool lasts until reboot. Read the boolean's description in semanage boolean -l before enabling it; some, like httpd_execmem, widen the attack surface considerably.
Ports: -m if it already exists β if the port is already assigned to another type, semanage port -a fails and -m modifies it instead. Check what else relies on that assignment first.
audit2allow hints β when a boolean would solve the denial, audit2allow prints a comment saying so above the rule. If you see that comment, use the boolean and discard the module.
When you do reach step four, the .te file is the thing to review β it is plain text listing exactly what you are granting. An allow httpd_t user_home_t:file read; line is a sign you skipped step one. An allow on shadow_t, etc_t write, or execmem deserves a second reviewer. Keep the module narrow by feeding audit2allow only the denials for one command and one reproduction, never the whole audit log.
Why Models Reach for setenforce 0 β and a Prompt That Stops It
Forum answers saying "just disable SELinux" outnumber good ones, and models learned from them. Without constraints, a model optimises for making the error disappear. You want it to identify which of the four fix classes applies β so tell it that explicitly:
$ ausearch -m AVC,USER_AVC -ts recent -i > /tmp/avc.txt
$ ls -Z /srv/www | head -20 > /tmp/labels.txt
$ ollama run llama3.2 "You are reviewing SELinux AVC denials on RHEL 9.
DENIALS:
$(cat /tmp/avc.txt)
CURRENT LABELS:
$(cat /tmp/labels.txt)
For each distinct denial:
1. Decode scontext, tcontext, tclass and the denied permission in one sentence.
2. Classify the cause as exactly one of: WRONG_LABEL, BOOLEAN, PORT, POLICY_GAP.
Consider them in that order and explain why earlier classes do not apply.
3. Give the fix: restorecon/semanage fcontext, setsebool -P, or semanage port.
Only for POLICY_GAP may you suggest audit2allow, and you must show the
expected .te rules.
Never suggest setenforce 0, disabling SELinux, or permissive mode as a fix.
If you are unsure a boolean exists, say UNVERIFIED instead of naming one."
Verify every boolean it names β models invent plausible boolean names. getsebool httpd_whatever returns an error for one that does not exist, and semanage boolean -l shows the real description.
Verify every type it names β seinfo -t | grep httpd lists the real types; a made-up httpd_web_content_t fails semanage fcontext immediately, which is a cheap test.
Ask what a boolean actually allows β sesearch -A -s httpd_t -t user_home_t -c file (from setools-console) shows the rules in policy, including which boolean guards each one.
Keep it local β AVC lines contain hostnames, paths, and usernames. A local model is the default for anything from production.
Debugging Without Turning SELinux Off
Sometimes an application hits a chain of denials: fix one, the next appears. Cycling through them while enforcing is slow. The right tool is a per-domain permissive mode, which leaves every other service confined:
# Only httpd_t becomes permissive; everything else stays enforced
$ semanage permissive -a httpd_t
# Exercise the application fully, then collect every denial at once
$ ausearch -m AVC -ts recent -i | grep permissive=1
# Fix them via the order above, then confine it again
$ semanage permissive -d httpd_t
$ semanage permissive -l # confirm nothing is left permissive
Why not setenforce 0 β it removes confinement from every process on the machine, including anything already compromised. Per-domain permissive limits the blast radius to the one service you are debugging.
Time-box it β a permissive domain left on is a silent hole that no monitoring will flag. Remove it the same day, and check semanage permissive -l during audits.
Then retest enforcing β a fix that only worked in permissive mode is not proven. Run the full workload again with the domain confined and confirm the log stays clean.
SELinux is one layer in a wider setup β our server hardening guide and file permissions guide cover the layers underneath it, and debugging systemd with AI covers the unit sandboxing that is often confused with it.
Frequently Asked Questions
Why does an AI tell me to run setenforce 0?
Because a large share of online answers to SELinux errors say exactly that, and models reproduce the most common answer. It makes the symptom vanish by removing confinement from the whole system. Tell the model explicitly that disabling or permissive mode is not an acceptable fix, and ask it to classify the cause as a label, boolean, port, or genuine policy gap.
How do I find SELinux denials on Linux?
Run ausearch -m AVC,USER_AVC -ts recent -i straight after reproducing the problem. If setroubleshoot is installed, journalctl -t setroubleshoot and sealert -a /var/log/audit/audit.log give grouped, explained versions. If nothing appears, dontaudit rules may be hiding the denial; semodule -DB disables them temporarily and semodule -B restores them.
Is it safe to use audit2allow?
It is safe when used narrowly and reviewed. Feed it only the denials from one command and one reproduction, read the generated .te file before installing the module, and discard it if audit2allow reports that a boolean would solve the denial. Running it over the whole audit log produces a module that grants far more than you intended.
What is the difference between restorecon and chcon?
chcon sets a label directly but does not record it in policy, so the next relabel or restorecon reverts it. restorecon resets files to the labels defined in policy. For non-standard paths, add a rule with semanage fcontext -a and then run restorecon so the label is permanent.
How do I debug one service without disabling SELinux?
Use semanage permissive -a with the service's domain, for example httpd_t. Only that domain stops being blocked, while denials are still logged with permissive=1. Collect them all, fix them properly, remove the exception with semanage permissive -d, and retest in enforcing mode.