This lab triggers a SELinux AVC denial, diagnoses it, and applies the right fix. By the end you will have the discipline for every MAC denial investigation.
Objective
By the end of this lab, you can:
- Trigger an AVC denial.
- Read the denial with ausearch.
- Decide the right fix.
- Apply the fix and verify the service works.
Architecture
You need a RHEL-family host (RHEL 9, Rocky 9, Alma 9, Fedora, or CentOS Stream 9) with:
- SELinux in enforcing mode.
- The
httpdpackage installed (or nginx). - A writable location for test content.
Tasks
Task 1: Verify SELinux is enforcing
sestatus
getenforce
Expected: enabled, enforcing. If not, fix it:
sudo setenforce 1
sudo sed -i 's/SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
Task 2: Create a mislabeled test file
sudo mkdir -p /var/www/html/test
sudo cp /usr/share/nginx/html/index.html /var/www/html/test/ 2>/dev/null || \
sudo cp /etc/hostname /var/www/html/test/index.html
# Mislabel: assign an arbitrary label
sudo chcon -t shadow_t /var/www/html/test/index.html
ls -lZ /var/www/html/test/
Task 3: Trigger the denial
If nginx is running:
sudo systemctl start nginx
curl http://localhost/test/
The curl fails with “Permission denied”. Check the AVC:
sudo ausearch -m avc -ts recent
Output should show a denial of nginx (or httpd) reading a
file of type shadow_t.
Task 4: Diagnose
ls -lZ /var/www/html/test/index.html
The file has type shadow_t but should be
httpd_sys_content_t. Diagnosis: mislabeled file.
Task 5: Fix with restorecon
sudo restorecon -v /var/www/html/test/index.html
ls -lZ /var/www/html/test/index.html
Now the file has httpd_sys_content_t.
Task 6: Verify
curl http://localhost/test/
Should succeed. Confirm no new AVCs:
sudo ausearch -m avc -ts recent
Task 7: A different scenario - a port outside the policy
Record the starting state before you change anything. The point
of this task is that 8888 is not in http_port_t, and you want
to have seen that for yourself:
sudo semanage port -l | grep '^http_port_t'
# http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000
Now move nginx to 8888. Stop the running instance first — the systemd-managed nginx from Task 3 still holds :80, and starting a second one by hand produces a bind conflict that looks nothing like an SELinux denial:
sudo systemctl stop nginx
sudo sed -i 's/listen\( *\)80\b/listen\18888/' /etc/nginx/nginx.conf
sudo grep -n 'listen' /etc/nginx/nginx.conf # confirm the edit
sudo nginx -t # syntax only; SELinux is not consulted here
sudo systemctl start nginx # expect this to FAIL
The start fails. nginx -t passed because it never binds a
socket — syntax checking and policy enforcement are different
questions, and this is the gap that makes port denials confusing.
Read the denial:
sudo systemctl status nginx --no-pager
sudo ausearch -m avc -ts recent | grep -E 'tclass=(tcp_socket|port)'
The AVC shows scontext=...:httpd_t denied name_bind on
tcontext=...:unreserved_port_t — nginx runs as httpd_t, which
is only permitted to bind ports labelled http_port_t.
Task 8: Fix the port
sudo semanage port -a -t http_port_t -p tcp 8888
sudo semanage port -l | grep '^http_port_t' # 8888 is now listed
sudo systemctl start nginx
ss -ltnp | grep 8888
Task 9: A third scenario - generate a policy
The first two scenarios had an answer already in the policy: a label to restore, a port to add. This one does not — the content lives somewhere the shipped policy has no rule for, and you have to write the rule.
Put it under /srv, not /var/www. Anything you create under
/var/www inherits httpd_sys_content_t from the existing
/var/www(/.*)? fcontext rule, so nginx is allowed to read it
and no denial ever happens — the task would appear to pass while
teaching nothing:
sudo mkdir -p /srv/myapp
echo ok | sudo tee /srv/myapp/index.html
# Label it as something httpd_t has no rule for
sudo semanage fcontext -a -t var_t '/srv/myapp(/.*)?'
sudo restorecon -Rv /srv/myapp
ls -lZ /srv/myapp/
Point nginx at it and reload:
sudo tee /etc/nginx/conf.d/myapp.conf >/dev/null <<'EOF'
server {
listen 8889;
location /myapp/ {
alias /srv/myapp/;
}
}
EOF
sudo semanage port -a -t http_port_t -p tcp 8889
sudo nginx -t && sudo systemctl reload nginx
curl -sI http://localhost:8889/myapp/
The request returns 403, not 404 — the file is there and nginx found it; the kernel refused the read. That distinction is the whole diagnostic: a 404 means you got the path wrong, a 403 with a matching AVC means policy stopped you.
sudo ausearch -m avc -ts recent
# scontext=...:httpd_t tcontext=...:var_t tclass=file denied { read open }
Generate a policy module:
sudo ausearch -m avc -ts recent | audit2allow -M nginx-myapp
# Review the .te file
cat nginx-myapp.te
# Install
sudo semodule -i nginx-myapp.pp
Verify:
curl -s http://localhost:8889/myapp/ # returns 'ok'
sudo ausearch -m avc -ts recent # no new denials
Task 10: Document the lab
SELinux MAC Lab
===============
Scenario 1 (mislabel):
- Symptom: nginx returns 403
- AVC: scontext=httpd_t tcontext=shadow_t
- Fix: restorecon
- Verification: curl succeeds
Scenario 2 (port):
- Symptom: nginx cannot bind 8888
- AVC: scontext=httpd_t tclass=port
- Fix: semanage port -a -t http_port_t -p tcp 8888
- Verification: nginx listens on 8888
Scenario 3 (policy gap):
- Symptom: nginx returns 403 (not 404) for /srv/myapp
- AVC: scontext=httpd_t tcontext=var_t tclass=file denied { read }
- Fix: audit2allow + semodule (labelling would be the better fix)
- Verification: curl succeeds, no new AVCs
Validation
- Scenario 1: the mislabeled file was fixed with
restorecon, andls -lZshowshttpd_sys_content_tagain. - Scenario 2: nginx failed to start on 8888 with an AVC naming
name_bindandunreserved_port_t, and started successfully aftersemanage port -a -t http_port_t -p tcp 8888. - Scenario 3: the 403 was accompanied by a matching AVC (a 403
with no AVC is an nginx permissions problem, not SELinux), and
semodule -l | grep nginx-myapplists the installed module. - All scenarios:
getenforcereportedEnforcingthroughout. A scenario that only works aftersetenforce 0is not a pass.
Cleanup
Revert every change, in reverse order. Note that the paths here
are the ones the lab actually created — /var/www/html/test from
Task 2 and /srv/myapp from Task 9:
sudo systemctl stop nginx
# Task 9: policy module, content, and its label rule
sudo semodule -r nginx-myapp 2>/dev/null
sudo rm -f /etc/nginx/conf.d/myapp.conf
sudo rm -rf /srv/myapp
sudo semanage fcontext -d '/srv/myapp(/.*)?' 2>/dev/null
# Tasks 7-8: the ports added to http_port_t
sudo semanage port -d -t http_port_t -p tcp 8888 2>/dev/null
sudo semanage port -d -t http_port_t -p tcp 8889 2>/dev/null
# Task 7: put nginx back on port 80
sudo sed -i 's/listen\( *\)8888\b/listen\180/' /etc/nginx/nginx.conf
# Task 2: the mislabelled test content
sudo rm -rf /var/www/html/test
sudo restorecon -R /var/www/
sudo nginx -t && sudo systemctl start nginx
ss -ltnp | grep ':80'
What you learned
- SELinux denials follow a pattern: AVC log tells you what is denied.
- The fix depends on the cause: mislabel, port, or policy gap.
- restorecon is the first fix to try for mislabeled files.
- audit2allow generates policies; review them carefully.