Linux Administration
SELinux Basics
In this lesson
SELinux (Security-Enhanced Linux) is a mandatory access control system built into the Linux kernel that adds a second layer of permission checks on top of the standard Unix file permissions. Even if a process runs as root, SELinux can prevent it from accessing files, network ports, or other resources it has not been explicitly granted. Originally developed by the NSA and shipped as standard on RHEL, Rocky Linux, and Fedora, SELinux is one of the most powerful and most frequently misunderstood security features in Linux.
DAC vs MAC — Why SELinux Exists
Linux's traditional permission model is called DAC (Discretionary Access Control) — the owner of a file decides who can access it. The weakness of DAC is that a compromised process running as a privileged user inherits all of that user's permissions. SELinux implements MAC (Mandatory Access Control) — the kernel enforces a policy that applies regardless of who owns a resource. A compromised nginx process cannot read /etc/shadow even if it is running as root, because SELinux policy says nginx is not allowed to access shadow password files.
Fig 1 — DAC checks ownership; MAC adds a second check based on SELinux labels regardless of user identity
Analogy: DAC is like a building where your employee badge grants access to all floors your job title allows. If someone steals your badge, they get all your access. MAC is like adding a second check at every door — a fingerprint scanner that verifies you are specifically authorised for that room, independent of your badge. Even a stolen badge cannot open a door you are not biometrically registered for.
SELinux Modes
SELinux operates in one of three modes. The mode determines whether policy violations are enforced or only logged — a crucial distinction when troubleshooting applications that were not written with SELinux in mind.
Policy violations are blocked and logged. The intended production mode. All access denials generate AVC (Access Vector Cache) denial messages in the audit log.
Violations are logged but not blocked. Useful for troubleshooting — the application runs unhindered while all policy violations are recorded for analysis.
SELinux is completely off. No logging, no enforcement. Requires a reboot to change from disabled to any other mode — filesystem relabelling is triggered on next boot.
# Check current SELinux mode and policy
sestatus
getenforce
# Temporarily switch to permissive (survives until reboot)
sudo setenforce 0 # 0 = permissive
sudo setenforce 1 # 1 = enforcing
# Permanently change mode — edit /etc/selinux/config
sudo sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
# Options: enforcing | permissive | disabled
# Changes take effect on next reboot
# Check current mode in the config file
cat /etc/selinux/config | grep "^SELINUX="
# Check which SELinux policy is loaded
cat /etc/selinux/config | grep "^SELINUXTYPE="
# Common policies: targeted (most systems), mls (multi-level security)# sestatus SELinux status: enabled SELinuxfs mount: /sys/fs/selinux SELinux mount point: /sys/fs/selinux Loaded policy name: targeted Current mode: enforcing Mode from config file: enforcing Policy MLS status: disabled Policy deny_unknown status: allowed Memory protection checking: actual (safe) Max kernel policy version: 33 # getenforce Enforcing
What just happened? sestatus confirmed SELinux is running in enforcing mode with the targeted policy. The targeted policy applies SELinux controls to specific high-risk daemons (nginx, postgresql, sshd, etc.) while allowing everything else to run unconfined — a practical balance between security and compatibility. The "Mode from config file" and "Current mode" both showing "enforcing" means no one has temporarily overridden the mode with setenforce.
Security Contexts — SELinux Labels
Every process, file, port, and user on a SELinux system carries a security context — a label with four fields that determines what the object can do and be accessed by. The most important field for day-to-day administration is the type field. SELinux policy rules are mostly expressed as "processes of type X may access resources of type Y."
Fig 2 — Security context anatomy: four colon-separated fields; type is the operationally significant field
# Show SELinux context of files — add -Z flag to standard commands
ls -laZ /var/www/html/
ls -Z /etc/shadow
# Show SELinux context of running processes
ps -eZ | grep nginx
ps auxZ | grep httpd
# Show SELinux context of network ports
sudo semanage port -l | grep http
# Show your current login context
id -Z
# Show SELinux context of a specific file
stat --printf="%C\n" /var/www/html/index.html
# View the full context of all files in a directory
ls -laZ /etc/ssh/# ls -laZ /var/www/html/ system_u:object_r:httpd_sys_content_t:s0 index.html system_u:object_r:httpd_sys_content_t:s0 style.css # ps -eZ | grep nginx system_u:system_r:httpd_t:s0 nginx: master process /usr/sbin/nginx system_u:system_r:httpd_t:s0 nginx: worker process # ls -Z /etc/shadow system_u:object_r:shadow_t:s0 /etc/shadow # sudo semanage port -l | grep "http " http_port_t tcp 80, 443, 488, 8008, 8009, 8443
What just happened? The labels tell the full SELinux story: nginx processes run with type httpd_t. The web content files in /var/www/html/ have type httpd_sys_content_t. SELinux policy allows httpd_t to read httpd_sys_content_t files. The shadow file has type shadow_t — policy does not grant httpd_t access to shadow_t, so even root-owned nginx cannot read it.
Reading and Resolving AVC Denials
When SELinux blocks an action, it writes an AVC denial (Access Vector Cache denial) to the audit log at /var/log/audit/audit.log. These messages contain everything needed to understand what was blocked and how to fix it. The audit2why and audit2allow tools translate raw AVC messages into human-readable explanations and policy suggestions.
# View recent SELinux denials from the audit log
sudo ausearch -m AVC -ts recent
sudo ausearch -m AVC,USER_AVC -ts today
# View denials in the system journal
sudo journalctl | grep "SELinux is preventing"
# The setroubleshoot-server package provides the best human-readable explanations
sudo dnf install setroubleshoot-server -y
sudo sealert -a /var/log/audit/audit.log
# Use audit2why to explain a specific denial
sudo ausearch -m AVC -ts recent | audit2why
# Generate a policy module to allow a specific denial (use with caution)
sudo ausearch -m AVC -ts recent | audit2allow -M mypolicy
sudo semodule -i mypolicy.pp
# Check if SELinux is causing an application failure
# Step 1: switch to permissive temporarily
sudo setenforce 0
# Step 2: reproduce the error
# Step 3: if it works now, SELinux was the cause — check audit.log
# Step 4: return to enforcing and fix properly
sudo setenforce 1# sudo ausearch -m AVC -ts recent | audit2why
type=AVC msg=audit(1741781234.123:892): avc: denied { read } for
pid=9821 comm="nginx" name="app.conf" dev="sda3" ino=131074
scontext=system_u:system_r:httpd_t:s0
tcontext=system_u:object_r:admin_home_t:s0 tclass=file
Was caused by:
Missing type enforcement (TE) allow rule.
You can use audit2allow to generate a loadable module to allow this access.
SELinux is preventing nginx (httpd_t) from reading /root/app.conf (admin_home_t).
The file has the wrong SELinux type for nginx to access it.
Solution: Move the file to /etc/nginx/ or relabel it:
restorecon -v /root/app.conf
OR
chcon -t httpd_config_t /root/app.conf
What just happened? audit2why translated the raw AVC denial into a clear diagnosis: nginx (running as httpd_t) tried to read a config file from /root/, which has the label admin_home_t — a type nginx is not allowed to read. The solution is not to disable SELinux but to fix the file's location or label. Moving the config to /etc/nginx/ or relabelling it with chcon is the correct fix.
Managing File Contexts and Booleans
The two most common reasons SELinux blocks legitimate applications are: wrong file context labels (the file is in the wrong location or was copied without preserving the label), and a disabled Boolean (a named policy toggle that controls optional behaviour). Both have safe, targeted fixes that do not require disabling SELinux.
File Context Labels
Files copied from one location to another inherit the source directory's label, not the destination's expected label. Use restorecon to reset to the correct type for the destination path.
# Fix wrong label on a file
restorecon -v /var/www/html/index.html
# Fix entire directory tree
restorecon -Rv /var/www/html/
# Show what label a path should have
matchpathcon /var/www/html/index.html
SELinux Booleans
Booleans toggle optional policy rules on or off. Common examples: allow nginx to connect to the network, allow Apache to write to its home dir, allow FTP to access home directories.
# List all booleans
getsebool -a | grep httpd
# Toggle a boolean (runtime — lost on reboot)
setsebool httpd_can_network_connect on
# Toggle permanently
setsebool -P httpd_can_network_connect on
# ── File context management ───────────────────────────────────────
# View the SELinux file context database — what type should /var/www/* have?
sudo semanage fcontext -l | grep "/var/www"
# Add a custom file context rule (for a non-standard web root)
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/myapp(/.*)?"
sudo restorecon -Rv /srv/myapp/
# Change a file's context temporarily (survives until restorecon is run)
sudo chcon -t httpd_sys_content_t /srv/myapp/index.html
# Reset a file to its correct default context
sudo restorecon -v /srv/myapp/index.html
# ── Boolean management ────────────────────────────────────────────
# Search booleans related to nginx/httpd
getsebool -a | grep httpd | grep -v "off$" # show only enabled booleans
# Common booleans for web servers
getsebool httpd_can_network_connect # allow nginx/Apache to make outbound connections
getsebool httpd_can_network_connect_db # allow httpd to connect to databases
getsebool httpd_enable_homedirs # allow httpd to serve from home directories
# Enable nginx to connect to a backend application server (e.g. Node.js, uWSGI)
sudo setsebool -P httpd_can_network_connect on# getsebool -a | grep httpd | grep connect httpd_can_network_connect --> off httpd_can_network_connect_db --> off httpd_can_network_relay --> off # sudo setsebool -P httpd_can_network_connect on # getsebool httpd_can_network_connect httpd_can_network_connect --> on # sudo semanage fcontext -l | grep "/var/www/html" /var/www/html(/.*)? all files system_u:object_r:httpd_sys_content_t:s0
What just happened? httpd_can_network_connect was off — explaining why nginx could not proxy requests to a backend service on another port. Setting it to on with -P made the change permanent. The semanage fcontext output confirmed the expected label for /var/www/html — any file placed there should have httpd_sys_content_t.
SELinux Administration — Common Scenarios
The following patterns cover the most frequent SELinux administration scenarios encountered in production. Each has a correct fix that maintains security — and an incorrect shortcut that administrators should recognise and avoid.
Wrong fix: setenforce 0. Right fix: restorecon -Rv /var/www/html/ to restore the correct label for that path.
Right fix: Add the port to the httpd port type: sudo semanage port -a -t http_port_t -p tcp 8443
Right fix: Register the new path: semanage fcontext -a -t httpd_sys_content_t "/srv/app(/.*)?" then restorecon -Rv /srv/app/
Right fix: Enable the network connect boolean: setsebool -P httpd_can_network_connect on. This is the most common SELinux issue on RHEL web servers.
# Complete workflow — nginx on a new Rocky Linux server serving /srv/myapp/
# 1. Register the correct context for the non-standard web root
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/myapp(/.*)?"
# 2. Apply labels
sudo restorecon -Rv /srv/myapp/
# 3. If nginx needs to proxy to a backend service
sudo setsebool -P httpd_can_network_connect on
# 4. If nginx needs to connect to a database (PHP apps, etc.)
sudo setsebool -P httpd_can_network_connect_db on
# 5. Verify no remaining denials
sudo ausearch -m AVC -ts today | audit2why
# Full audit: relabel the entire filesystem (useful after major changes — slow!)
# sudo touch /.autorelabel && sudo rebootNever Set SELinux to Disabled to "Fix" Application Problems
Disabling SELinux — changing SELINUX=enforcing to SELINUX=disabled — strips away a substantial layer of kernel-level protection and requires a full filesystem relabelling on re-enablement. It is never the correct solution for a specific application problem. The correct approach is always: read the AVC denial, use audit2why to understand it, and fix the file context or enable the relevant boolean. Temporarily switching to permissive with setenforce 0 is acceptable during troubleshooting, but must always be followed by returning to enforcing mode with the proper fix applied.
Lesson Checklist
getenforce / sestatus to check the current mode, and I only use setenforce 0 temporarily during troubleshooting — never as a permanent fix
ls -Z and ps -eZ, and I understand the four context fields — especially the type field
ausearch -m AVC | audit2why to diagnose denials, and I fix them with restorecon, semanage fcontext, or setsebool -P
setsebool -P httpd_can_network_connect on when nginx cannot proxy to a backend
Teacher's Note
The most important habit to develop with SELinux is to always run ausearch -m AVC -ts recent | audit2why before reaching for setenforce 0. In 95% of cases the denial message will point to a missing boolean or a wrong file context — both fixable in under a minute. The remaining 5% are genuinely novel access patterns that warrant a custom policy module via audit2allow. Disabling SELinux entirely is never in either category.
Practice Questions
1. You deploy a new application on a Rocky Linux 9 server. The application files are in /opt/myapp/public/ and nginx is configured to serve from that directory. Nginx returns 403 Forbidden. ls -Z /opt/myapp/public/ shows the type is default_t. Walk through the complete fix.
default_t files. Fix permanently: sudo semanage fcontext -a -t httpd_sys_content_t '/opt/myapp/public(/.*)?' then apply: sudo restorecon -Rv /opt/myapp/public/. Verify: ls -Z /opt/myapp/public/ should now show httpd_sys_content_t. Do not use chcon alone — the label would revert after a relabel. Using semanage writes the rule to the policy database making it survive reboots and restorecon.
2. A colleague suggests disabling SELinux to fix an nginx proxy issue on a production RHEL server, arguing "it's just one server and it's inside the firewall." Give a technical counter-argument explaining what SELinux protects against that the firewall does not, and describe the two-minute correct fix for the most common nginx proxy problem.
sudo setsebool -P httpd_can_network_connect on — this allows the httpd_t domain to make outbound TCP connections. Check with getsebool httpd_can_network_connect first to confirm it is off.
3. Explain the difference between using chcon to change a file's SELinux context and using semanage fcontext followed by restorecon. Which should you use in production and why?
chcon changes the label directly on the file — it is temporary and will be reverted if anyone runs restorecon or a full filesystem relabel. semanage fcontext writes a persistent rule to the SELinux policy database specifying what context a path should have; restorecon then applies it. Use semanage + restorecon in production — it survives reboots, relabels, and OS upgrades. Reserve chcon for quick testing only.
Lesson Quiz
1. An nginx process is running as root and tries to read /etc/shadow. SELinux is in enforcing mode. What happens?
2. What does SELinux permissive mode do differently from enforcing mode?
3. A RHEL server running nginx as a reverse proxy to a Node.js app on port 3000 returns 502 Bad Gateway. curl localhost:3000 works. What is the most likely SELinux cause and fix?
Up Next
Lesson 31 — Linux Security Best Practices
Hardening Linux servers against attack — audit tools, intrusion detection, kernel parameters, and security frameworks