sysadmin · June 29, 2026 · 15 min read

SELinux, part one.
reading denials and fixing file contexts.

httpd serves files from /srv/webcontent but returns 403 on every request. Disabling SELinux fixes it, but you are not going to do that. Diagnose the denial and fix the file context properly.

// what we’re getting into
  1. The ticket
  2. Concept review. Modes and contexts.
  3. Reproduce the 403
  4. Confirm SELinux is the cause
  5. Use permissive to prove it
  6. The temporary fix, and why it does not last
  7. The permanent fix with semanage fcontext
  8. Exam questions
  9. Final checklist: confirm everything works

webadmin stood up the web tier on server1, pointed httpd at /srv/webcontent, the same SGID directory from the shared-folder ticket back on day three, and every request comes back the same way: 403 Forbidden. Not some of them. All of them. The files are there, httpd is listening, and the permissions are fine. The one thing that makes it work is setenforce 0, which is exactly the thing you are not going to leave in place.

That last detail is the whole diagnosis. If turning SELinux off fixes it, SELinux is doing its job and refusing httpd access to files carrying the wrong label. Anything you create under /srv lands with a generic default_t type, and httpd is only allowed to read httpd_sys_content_t. So this is not a permissions bug or an httpd bug. It is a label, and the real work is relabeling those files in a way a later restorecon will not silently undo.

Confirm the ground you are standing on

bash
getenforce                        # Enforcing
systemctl is-active httpd         # active
ls -Z /srv/webcontent 2>/dev/null

The three modes and the one field that matters

bash
man selinux
man restorecon

There are three modes. Enforcing blocks and logs. Permissive blocks nothing but still logs, which is why it is the mode you diagnose in. Disabled is off and needs a reboot to change, so leave it alone. Every file carries a context of four colon-separated fields, and on this exam the only one you touch is the third, the type.

the context on /var/www/html
system_u:object_r:httpd_sys_content_t:s0
         ^^^^^^^^  ^^^^^^^^^^^^^^^^^^^
         role      type  <- the field that decides access

httpd_sys_content_t     httpd may read it
httpd_sys_rw_content_t  httpd may write it
default_t               what /srv gets, and httpd cannot read it

Watch the 403 happen

Point httpd at the directory and fetch it. The permissions are wide open (Require all granted), so when this still returns 403, permissions are ruled out and the label is the only suspect left.

bash
sudo tee /etc/httpd/conf.d/server1.conf << 'EOF'
DocumentRoot "/srv/webcontent"
<Directory "/srv/webcontent">
    Require all granted
</Directory>
EOF
sudo systemctl restart httpd

curl -I http://localhost/
# HTTP/1.1 403 Forbidden
# Server: Apache/2.4.62 (Red Hat Enterprise Linux)

Read the denial, then read the labels

The denial is written to the audit log in plain sight. It names httpd as the source, the file it wanted, and the type it choked on. Then put your file’s label next to the default web root’s label and the mismatch is right there.

bash
sudo ausearch -m AVC -ts recent | tail -3
# type=AVC msg=audit(...): avc:  denied  { getattr } for  pid=1893
#   comm="httpd" path="/srv/webcontent/index.html"
#   scontext=system_u:system_r:httpd_t:s0
#   tcontext=unconfined_u:object_r:default_t:s0  tclass=file

ls -Z /srv/webcontent/index.html
# unconfined_u:object_r:default_t:s0            <- httpd is refused this
ls -Z /var/www/html/
# system_u:object_r:httpd_sys_content_t:s0      <- this is the target

There it is: tcontext=…:default_t:s0 in the denial, and default_t on the file. httpd wanted httpd_sys_content_t and got the /srv default instead.

One flip to prove it, then flip back

You do not have to guess. Permissive stops blocking but keeps logging, so if the 403 turns into a 200 the instant you flip it, SELinux was the only thing in the way. Then go straight back to enforcing, because leaving it in permissive is the same cop-out as disabling it.

bash
sudo setenforce 0
curl -I http://localhost/
# HTTP/1.1 200 OK        <- same files, same httpd, only SELinux changed
sudo setenforce 1

The fix that looks right and then rots

The obvious move is chcon, which stamps a new label straight onto the file. It works immediately, which is the trap. chcon only changes the file, not the policy, so the file’s “correct” label according to SELinux is still default_t. The next restorecon that runs, and one runs on relabels, reboots, and plenty of admin tasks, resets it and the 403 comes back. Watch it happen:

bash
sudo chcon -R -t httpd_sys_content_t /srv/webcontent
curl -I http://localhost/    # 200 OK... for now

sudo restorecon -Rv /srv/webcontent
# Relabeled /srv/webcontent/index.html from
#   unconfined_u:object_r:httpd_sys_content_t:s0 to ...:default_t:s0
curl -I http://localhost/    # 403 Forbidden again

The fix that survives

The durable move changes the policy, not the file. semanage fcontext -a records a rule that says “anything at this path should be httpd_sys_content_t,” and then restorecon enforces that rule. Now restorecon is on your side: because the policy agrees with the label, every future run keeps it.

bash
sudo dnf install -y policycoreutils-python-utils
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/webcontent(/.*)?"
sudo restorecon -Rv /srv/webcontent
# Relabeled /srv/webcontent/index.html from ...:default_t:s0 to
#   ...:httpd_sys_content_t:s0

curl -I http://localhost/    # 200 OK, and this time it stays
sudo restorecon -Rv /srv/webcontent    # run it again: no change, it holds
note: The (/.*)? on the end is the piece people drop, and it is a regex meaning “this directory or anything under it.” Leave it off and you label the directory but not the index.html inside it, so the page still 403s and you spend ten minutes confused.

Exam questions

Write the command first.

Q1. Show the current SELinux mode and the mode configured for boot.

Q2. Temporarily switch SELinux to permissive and verify without rebooting.

Q3. A web service cannot read files in /webdir due to SELinux. Fix the context permanently so it survives restorecon and reboots.

Answers.

bash
# A1
getenforce; sestatus; cat /etc/selinux/config
# A2
sudo setenforce 0; getenforce
# A3
sudo semanage fcontext -a -t httpd_sys_content_t "/webdir(/.*)?"
sudo restorecon -Rv /webdir
ls -Z /webdir

Final checklist: confirm everything works

If every check passes, the ticket is done.

bash
# 1. 403 reproduced, then confirmed as SELinux via permissive
# 2. semanage fcontext + restorecon used, not just chcon
sudo semanage fcontext -l | grep webcontent

# 3. label survives a fresh restorecon
sudo restorecon -Rv /srv/webcontent; ls -Z /srv/webcontent/index.html

# 4. enforcing restored and httpd serves 200 OK
getenforce; curl -I http://localhost/

# 5. all three exam commands written from scratch
# 6. tracker entry checked off

Reply to webadmin: fixed with semanage fcontext -a -t httpd_sys_content_t ‘/srv/webcontent(/.*)?’ then restorecon -R. Permanent, SELinux back to enforcing, curl returns 200.

next post
SELinux part two: ports, booleans, and audit2allow