Diagnosing an SELinux "denied" Problem on a Real Service
Nginx returns 403 for a site moved to a non-standard directory, even though every Unix permission looks correct. Diagnosing it's actually SELinux, reading the denial, and fixing it permanently with semanage fcontext + restorecon instead of the chcon shortcut or disabling SELinux.
Goal: move a site’s document root to a non-standard location, hit a 403 that has nothing to do with Unix permissions, confirm SELinux is actually the cause rather than guessing, and fix it the way that survives a reboot and a relabel - not the way that just makes the symptom disappear for now.
Why this is worth doing deliberately: this exact shape of problem - permissions look fine, config looks fine, it still doesn’t work - is the single most common way people conclude “SELinux is broken” and disable it. It isn’t broken; it’s doing precisely what it’s configured to do, and the fix is two commands once the actual cause is confirmed instead of assumed.
1. The setup: serving from a non-standard directory
sudo mkdir -p /srv/webdata
echo "<h1>It works (allegedly)</h1>" | sudo tee /srv/webdata/index.html
sudo chown -R nginx:nginx /srv/webdata # correct ownership
sudo chmod -R 755 /srv/webdata # correct permissions
# /etc/nginx/conf.d/webdata.conf
server {
listen 80;
server_name webdata.example.com;
root /srv/webdata; # not the default /usr/share/nginx/html
index index.html;
}
sudo nginx -t && sudo systemctl reload nginx # config syntax is valid, reload applies it
Ownership is correct, permissions are correct, the config itself is valid - by every normal Unix measure, this should already work.
2. The symptom
curl -I http://webdata.example.com/
# HTTP/1.1 403 Forbidden
A 403 with chmod 755 and correct ownership already in place is the signature of this exact problem - if it were a real permissions issue, it would be a different error or it simply wouldn’t have passed the ls -l check in step 1 at all.
3. Confirm it’s actually SELinux, don’t assume it
getenforce # Enforcing
sudo setenforce 0 # temporarily permissive - diagnostic only, see the warning below
curl -I http://webdata.example.com/ # HTTP/1.1 200 OK - confirms SELinux was the cause
sudo setenforce 1 # back to enforcing immediately - this proved the cause, it isn't the fix
setenforce 0here is a diagnostic step, not a fix - its only purpose is answering “is SELinux actually involved at all” with a yes/no before spending time on the wrong theory. Re-enable enforcing mode immediately after this check, in the same breath as running it - the real fix happens in sections 5-6, with SELinux still fully enforcing.
4. Read the actual denial, don’t just trust the toggle test
sudo ausearch -m avc -ts recent
type=AVC msg=audit(...): avc: denied { read } for pid=5821 comm="nginx"
name="index.html" dev="sda1" ino=131082
scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:default_t:s0
tclass=file
scontext=httpd_t (what nginx runs as) against tcontext=default_t (what /srv/webdata actually got labeled, since it’s outside any directory policy already knows about) is the whole diagnosis in two lines - nginx is confined to reading httpd_sys_content_t-labeled files, and this directory isn’t one.
5. See the mismatch directly
ls -Zd /srv/webdata /usr/share/nginx/html
unconfined_u:object_r:default_t:s0 /srv/webdata
system_u:object_r:httpd_sys_content_t:s0 /usr/share/nginx/html
/srv/webdata landed on the generic default_t type because nothing in SELinux’s file-context policy knows that path should be treated as web content - it has nothing to do with where /usr/share/nginx/html itself is, only with what policy has been told about each specific path.
6. Fix it permanently - not with chcon
sudo chcon -t httpd_sys_content_t -R /srv/webdata # works immediately - and is the wrong fix, see below
This single command would make the 403 disappear right now. It’s also exactly the trap: chcon changes the label in place without writing anything to the policy database, so the next restorecon -R /, a semanage operation elsewhere, or a full relabel (triggered by touch /.autorelabel - see the boot process cheat sheet) reverts it back to default_t with no warning, and the exact same 403 returns days or weeks later with no obvious cause. The fix that actually survives:
sudo semanage fcontext -a -t httpd_sys_content_t '/srv/webdata(/.*)?' # teach policy about this path, permanently
sudo restorecon -Rv /srv/webdata # apply the now-correct rule to existing files
restorecon -v prints every file it actually relabels - seeing /srv/webdata/index.html listed confirms the rule applied, not just that the command ran without error.
7. Confirm the fix holds with SELinux still enforcing
getenforce # Enforcing - never left permissive to "test" the real fix
curl -I http://webdata.example.com/ # HTTP/1.1 200 OK
This is the step that actually proves the fix, as opposed to step 3’s temporary permissive check - the site works now with SELinux doing its normal job, not because enforcement was switched off.
Verification - prove it’s actually permanent
sudo semanage fcontext -l | grep webdata # the policy rule is there, not just applied once
sudo restorecon -Rv /srv/webdata # run it again - should report NOTHING changed this time
sudo ausearch -m avc -ts recent | grep webdata # confirm no new denials after re-testing the site
A second restorecon -Rv reporting zero changes is the real confirmation - it means the files already match policy exactly, so any future relabel or restorecon -R / leaves this directory untouched instead of reverting it.
Safety notes
setenforce 0is for confirming a diagnosis, never for resolving one - re-enable enforcing in the same breath as disabling it, and do the actual fix with enforcement back on.chconis not wrong to use for a quick same-session test, but treat anything fixed with onlychconas still broken - it reverts on the next relabel with no warning.semanage fcontext+restoreconis the version of this fix that’s actually done.- Scope
semanage fcontextrules andrestoreconruns to the exact path involved - a rule or a relabel aimed too broadly (especially anything starting from/) affects far more than the one directory being fixed. - A denial doesn’t automatically mean “find the type and relabel it” - occasionally the correct fix is a boolean (
httpd_can_network_connectfor a reverse proxy, for example) rather than a context change;ausearch’stclassand the specific permission denied (name_connect,read, etc.) usually point at which one applies. - If
/usr/share/nginx/htmlis no longer used at all, it’s still a legitimatehttpd_sys_content_t-labeled path - don’t repurpose it for something that needs a different context without relabeling it too, or a later service expecting the original type there will fail in a way that looks unrelated to this change.