SELinux (Security-Enhanced Linux) is a mandatory access control (MAC) framework built into the Linux kernel, developed by the NSA and now maintained as a core part of RHEL. Unlike traditional discretionary access control (DAC) where file permissions determine access, SELinux enforces policies that confine every process to the minimum resources it legitimately needs — even if the process is running as root. Many administrators disable SELinux at the first sign of a denial, but this removes a powerful layer of defense-in-depth. This tutorial shows you how to work with SELinux on RHEL 9 rather than around it.
Prerequisites
- A RHEL 9 server with root or sudo access
- SELinux installed and enabled (it is by default on RHEL 9)
- The
policycoreutils,policycoreutils-python-utils, andsetroubleshoot-serverpackages
Step 1 — Check the Current SELinux Status
Before making any changes, understand the current state of SELinux on your system:
# Quick check: Enforcing, Permissive, or Disabled
getenforce
# Detailed status including policy type and file system mount options
sestatus
RHEL 9 ships in Enforcing mode with the targeted policy. This is the correct production configuration and should not be changed without a specific, well-understood reason.
The three modes are:
- Enforcing — SELinux policy is active and denials are blocked and logged.
- Permissive — Denials are logged but not blocked. Useful for troubleshooting.
- Disabled — SELinux is completely inactive. Switching back to Enforcing requires a full filesystem relabel on next boot.
Step 2 — Switch Between Enforcing and Permissive Mode
You can toggle between Enforcing and Permissive temporarily (survives until reboot) with setenforce:
# Switch to Permissive (for troubleshooting only — do NOT leave this way)
sudo setenforce 0
getenforce # outputs: Permissive
# Switch back to Enforcing
sudo setenforce 1
getenforce # outputs: Enforcing
To make the mode persistent across reboots, edit /etc/selinux/config:
sudo nano /etc/selinux/config
# Set the policy type and enforcement mode
SELINUX=enforcing # enforcing | permissive | disabled
SELINUXTYPE=targeted # do not change unless you know what you're doing
Never set SELINUX=disabled in production. Disabling SELinux removes kernel-level confinement of all processes and cannot be reversed without a reboot and full filesystem relabeling.
Step 3 — Understand and Inspect SELinux Contexts
Every file, process, port, and network socket has an SELinux security context in the format user:role:type:level. The type component is what the policy rules typically match on.
# View file contexts
ls -Z /var/www/html/
ls -Z /etc/ssh/sshd_config
# View process contexts
ps -eZ | grep httpd
ps -eZ | grep sshd
# View port contexts
sudo semanage port -l | grep http
Step 4 — Manage SELinux Booleans
SELinux booleans are on/off switches that enable or disable specific policy rules without requiring a full policy recompile. They are the correct way to grant additional permissions to confined services.
# List all booleans and their current state
getsebool -a
# Search for httpd-related booleans
getsebool -a | grep httpd
# Enable a boolean (allows Apache to make network connections to backends)
sudo setsebool -P httpd_can_network_connect on
# Allow HTTPD to read user home directories
sudo setsebool -P httpd_enable_homedirs on
The -P flag makes the change persistent across reboots. Without it, the boolean resets on reboot.
Step 5 — Diagnose and Fix SELinux Denials with audit2allow
When an application fails mysteriously on RHEL 9, SELinux is often the cause. Use ausearch to find AVC (Access Vector Cache) denial messages in the audit log and audit2allow to generate a policy module that permits the denied action.
# Search the audit log for recent AVC denials
sudo ausearch -m AVC -ts recent
# Generate a human-readable explanation with sealert
sudo sealert -a /var/log/audit/audit.log
# Automatically generate and install a custom policy module
sudo ausearch -m AVC -ts recent | audit2allow -M mypolicy
sudo semodule -i mypolicy.pp
# Verify the module was installed
sudo semodule -l | grep mypolicy
Before blindly applying a generated module, review the mypolicy.te (type enforcement) file that audit2allow creates. Understanding what permission you are granting helps you decide whether a boolean, a file context fix, or a custom module is the right solution.
Step 6 — Restore File Contexts and Manage Port Contexts
Files copied from outside their expected location often have incorrect SELinux contexts, causing access denials even though the Unix permissions look fine. Use restorecon to reset contexts to their policy-defined defaults:
# Restore contexts recursively on a directory
sudo restorecon -Rv /var/www/html/
# Check what the context should be without changing it
sudo restorecon -Rvn /etc/nginx/
If a service needs to listen on a non-standard port, add the port to the correct SELinux port type rather than disabling SELinux:
# Allow Apache/Nginx to listen on port 8080
sudo semanage port -a -t http_port_t -p tcp 8080
# Verify the change
sudo semanage port -l | grep http_port_t
# Allow custom SSH port (e.g., 2222)
sudo semanage port -a -t ssh_port_t -p tcp 2222
Conclusion
SELinux is one of the most powerful security features in RHEL 9, and learning to work with it rather than disabling it pays long-term dividends. The workflow for any SELinux problem is always the same: check the audit log with ausearch, identify whether a boolean, file context relabel, or port context addition solves the issue, and reach for audit2allow only when a custom policy module is genuinely needed. Keeping SELinux in Enforcing mode ensures that even a successfully exploited vulnerability in a web server or database daemon cannot escalate beyond its confined context — a protection that no amount of firewall rules can replicate.
Next steps: How to Harden SSH on RHEL 9, How to Configure Auditd for Linux System Auditing, and How to Set Up ClamAV Antivirus on RHEL 9.