SELinux: Stop Disabling It and Start Understanding It on RHEL
π― Key Takeaways
- SELinux: Stop Disabling It and Start Understanding It on RHEL
- Why Admins Disable SELinux (And Why That Is a Mistake)
- The Three SELinux Modes: Enforcing, Permissive, and Disabled
- SELinux Contexts Explained Simply
- Reading SELinux Denial Logs
π Table of Contents
- SELinux: Stop Disabling It and Start Understanding It on RHEL
- Why Admins Disable SELinux (And Why That Is a Mistake)
- The Three SELinux Modes: Enforcing, Permissive, and Disabled
- SELinux Contexts Explained Simply
- Reading SELinux Denial Logs
- Common SELinux Denial Scenarios and Fixes
- restorecon: The Most Useful Command You Are Not Using
- chcon vs semanage fcontext: Which One to Use and Why
- audit2allow: Generate Policy from Denials
- semanage boolean: Toggle Features Without Writing Policy
- Real Scenario: nginx Serving Files From /data
- Common Beginner Mistakes on RHEL
- Conclusion
SELinux: Stop Disabling It and Start Understanding It on RHEL
If you have ever searched for a fix to a broken service on RHEL and the top answer said “just run setenforce 0” or “set SELINUX=disabled in /etc/selinux/config”, you are not alone. Disabling SELinux is the most common mistake new RHEL administrators make, and it is also one of the most dangerous. SELinux is not a bug. It is one of the most powerful security layers built into the Linux kernel, and Red Hat ships it enabled and enforcing by default for good reason.
π Table of Contents
- SELinux: Stop Disabling It and Start Understanding It on RHEL
- Why Admins Disable SELinux (And Why That Is a Mistake)
- The Three SELinux Modes: Enforcing, Permissive, and Disabled
- Enforcing Mode
- Permissive Mode
- Disabled Mode
- SELinux Contexts Explained Simply
- Reading SELinux Denial Logs
- Reading audit.log Directly
- Using ausearch for Cleaner Output
- Using sealert for Human-Readable Explanations
- Common SELinux Denial Scenarios and Fixes
- Scenario 1: Web Server Cannot Read Files (Wrong Context)
- Scenario 2: Custom Port Blocked
- Scenario 3: Service Cannot Write to a Directory
- restorecon: The Most Useful Command You Are Not Using
- chcon vs semanage fcontext: Which One to Use and Why
- chcon β Temporary Context Change
- semanage fcontext β Permanent Context Rules
- audit2allow: Generate Policy from Denials
- semanage boolean: Toggle Features Without Writing Policy
- Real Scenario: nginx Serving Files From /data
- Common Beginner Mistakes on RHEL
- Conclusion
This guide is written for administrators coming from Ubuntu or Debian where AppArmor is optional and rarely enforced, or from environments where SELinux was disabled on day one. By the end, you will understand what SELinux actually does, how to read its denial logs, and how to fix real-world problems the right way β without ever touching that setenforce command again.
Why Admins Disable SELinux (And Why That Is a Mistake)
The frustration is understandable. You deploy a service, it fails, you check the logs, nothing obvious stands out, then you disable SELinux and suddenly everything works. The problem is not SELinux β it is that nobody explained what SELinux was actually blocking and why.
When you disable SELinux, you are removing mandatory access controls from your entire system. This means a compromised web server process can now read /etc/shadow, write to /root, or spawn shells β things that SELinux would have blocked entirely. The tradeoff is not worth it. Red Hat builds RHEL around SELinux, and the entire policy framework is designed so that legitimate software works when configured correctly.
The real issue is that SELinux denials are cryptic if you do not know what to look for. This guide fixes that.
The Three SELinux Modes: Enforcing, Permissive, and Disabled
SELinux operates in one of three modes. Understanding what each mode does is fundamental before anything else.
Enforcing Mode
This is the default on RHEL. In enforcing mode, SELinux actively blocks any access that violates its policy and logs the denial to /var/log/audit/audit.log. This is the mode your production systems should always run in.
Permissive Mode
In permissive mode, SELinux logs violations exactly as it would in enforcing mode, but it does not block them. This is invaluable for troubleshooting because you can see everything SELinux would have denied without breaking your service. Use permissive mode to diagnose, then fix the underlying issue and return to enforcing.
Disabled Mode
Disabled completely removes SELinux from the kernel. This requires a reboot and, critically, it means all files on the system lose their SELinux labels. Re-enabling SELinux after it has been disabled requires a full filesystem relabel on the next boot, which can take a very long time on large systems. Never disable SELinux in production.
Check the current mode with:
$ getenforce
Enforcing
$ 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: enabled
Policy deny_unknown status: allowed
Memory protection checking: actual (secure)
Max kernel policy version: 33
To temporarily switch to permissive for troubleshooting (no reboot required, reverts after reboot):
# Switch to permissive temporarily
setenforce 0
# Switch back to enforcing
setenforce 1
To make a permanent mode change, edit /etc/selinux/config:
# /etc/selinux/config
SELINUX=enforcing # Change to permissive if needed for diagnosis
SELINUXTYPE=targeted
SELinux Contexts Explained Simply
Every file, process, port, and user on an SELinux system has a context β a label that defines what it is and what it is allowed to do. This is the core concept that makes everything else make sense.
A context looks like this:
system_u:object_r:httpd_content_t:s0
It has four fields separated by colons:
- user (
system_u) β the SELinux user. Not the same as Linux users. Most files aresystem_uorunconfined_u. - role (
object_r) β mostly relevant for process contexts. Files useobject_r. - type (
httpd_content_t) β this is the part that matters most. Type enforcement is how SELinux makes access decisions. A process with typehttpd_tcan read files labeledhttpd_content_t. - level (
s0) β sensitivity level used in Multi-Level Security (MLS). For most RHEL deployments, this iss0and you can ignore it.
Check the context of files with ls -Z and processes with ps -Z:
$ ls -Z /var/www/html/
unconfined_u:object_r:httpd_sys_content_t:s0 index.html
$ ps -Z -C httpd
LABEL PID TTY STAT TIME COMMAND
system_u:system_r:httpd_t:s0 1234 ? Ss 0:00 /usr/sbin/httpd
The Apache process runs as httpd_t and it can read files labeled httpd_sys_content_t. If you put a file somewhere with a different type label β say user_home_t β Apache cannot read it, even if the Linux file permissions say 755. That is the entire model.
Reading SELinux Denial Logs
When SELinux blocks something, it writes a detailed AVC (Access Vector Cache) denial to /var/log/audit/audit.log. Learning to read these is the most important skill in this guide.
Reading audit.log Directly
$ grep AVC /var/log/audit/audit.log | tail -5
type=AVC msg=audit(1709650123.456:892): avc: denied { read } for pid=2341 comm="nginx" name="index.html" dev="sda1" ino=131073 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0
Breaking this down:
denied { read }β SELinux denied a read operationcomm="nginx"β the nginx process tried to do itname="index.html"β on this filescontext=...httpd_tβ the nginx process has this context (source)tcontext=...user_home_tβ the file has this context (target)tclass=fileβ the object class is a regular file
The denial is telling you exactly what happened: nginx (running as httpd_t) tried to read a file labeled user_home_t, which is not allowed by policy.
Using ausearch for Cleaner Output
The ausearch tool filters audit logs and makes them easier to read:
# Show all SELinux denials from today
$ ausearch -m avc -ts today
# Show denials for a specific service
$ ausearch -m avc -c nginx
# Show denials in the last hour
$ ausearch -m avc -ts recent
----
time->Fri Mar 6 10:23:45 2026
type=AVC msg=audit(1741254225.123:445): avc: denied { read } for
pid=3421 comm="nginx" name="data" dev="sda1" ino=262145
scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:var_t:s0 tclass=dir permissive=0
Using sealert for Human-Readable Explanations
Install the setroubleshoot-server package to get sealert, which translates AVC denials into plain English with suggested fixes:
$ dnf install setroubleshoot-server -y
$ sealert -a /var/log/audit/audit.log
100% done
found 1 alerts in /var/log/audit/audit.log
--------------------------------------------------------------------------------
SELinux is preventing nginx from read access on the directory data.
***** Plugin restorecon (99.5 confidence) suggests ************************
If you want to fix the label:
/data is mislabeled on your system.
Fix with:
# /sbin/restorecon -v /data
When sealert is confident about the fix, it tells you exactly what command to run. This is your best friend when getting started.
Common SELinux Denial Scenarios and Fixes
Scenario 1: Web Server Cannot Read Files (Wrong Context)
You create a new document root at /data/website and nginx refuses to serve files. The files have correct Linux permissions, but SELinux is blocking access.
# The directory was created manually, so it has the wrong context
$ ls -Zd /data/website
unconfined_u:object_r:var_t:s0 /data/website/
# Nginx expects httpd_sys_content_t
# The fix: set the correct context permanently
$ semanage fcontext -a -t httpd_sys_content_t "/data/website(/.*)?"
$ restorecon -Rv /data/website/
Relabeled /data/website from unconfined_u:object_r:var_t:s0 to system_u:object_r:httpd_sys_content_t:s0
Relabeled /data/website/index.html from unconfined_u:object_r:var_t:s0 to system_u:object_r:httpd_sys_content_t:s0
Scenario 2: Custom Port Blocked
You configure nginx to listen on port 8088 instead of the standard 80 or 443. SELinux blocks it because port 8088 is not labeled as an HTTP port.
# Check what ports are allowed for httpd
$ semanage port -l | grep http
http_cache_port_t tcp 8080, 8118, 8123, 10001-10010
http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000
# Port 8088 is not listed β add it
$ semanage port -a -t http_port_t -p tcp 8088
# Verify
$ semanage port -l | grep http_port_t
http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8088, 8443, 9000
Scenario 3: Service Cannot Write to a Directory
A custom application needs to write logs to /opt/myapp/logs. The service fails because the directory has the wrong context.
# Check what context the app process runs as
$ ps -Z -C myapp
system_u:system_r:myapp_t:s0 4521 ? Ss 0:00 /opt/myapp/bin/myapp
# The logs directory has a generic context
$ ls -Zd /opt/myapp/logs
unconfined_u:object_r:usr_t:s0 /opt/myapp/logs/
# If your app uses httpd_t (runs as Apache/Nginx), allow writing
$ semanage fcontext -a -t httpd_log_t "/opt/myapp/logs(/.*)?"
$ restorecon -Rv /opt/myapp/logs/
restorecon: The Most Useful Command You Are Not Using
restorecon resets file contexts back to what the SELinux policy says they should be. This is the correct fix for the majority of SELinux file-related denials. When you copy a file instead of moving it, when you create directories in non-standard paths, or when you restore from backup β contexts get wrong. restorecon fixes them.
# Restore context on a single file
$ restorecon -v /var/www/html/index.html
Relabeled /var/www/html/index.html from unconfined_u:object_r:user_home_t:s0 to system_u:object_r:httpd_sys_content_t:s0
# Restore context on an entire directory tree
$ restorecon -Rv /var/www/html/
# Dry run β see what would change without changing it
$ restorecon -Rvn /var/www/html/
Common mistake: Using cp instead of mv to move files. When you copy a file, it inherits the context of the destination directory. When you move it, it keeps its original context. Neither is always right. After moving or copying files into service directories, always run restorecon.
chcon vs semanage fcontext: Which One to Use and Why
Both commands change SELinux contexts, but they work very differently and this distinction matters enormously.
chcon β Temporary Context Change
chcon changes the context of a file directly. However, this change is not persistent. If you run restorecon or if the system performs a relabel, the context reverts to whatever the policy says it should be.
# This works but will NOT survive restorecon
$ chcon -t httpd_sys_content_t /data/website/index.html
Use chcon only for quick testing to confirm that a context change fixes your problem. Never rely on it for production.
semanage fcontext β Permanent Context Rules
semanage fcontext adds a rule to the SELinux policy database that says “files matching this path pattern should have this context.” When you then run restorecon, it applies the rule. This change survives reboots and relabels.
# Add a permanent rule (does not change existing files yet)
$ semanage fcontext -a -t httpd_sys_content_t "/data/website(/.*)?"
# Apply the rule to existing files
$ restorecon -Rv /data/website/
# List your custom fcontext rules
$ semanage fcontext -l -C
SELinux fcontext type Context
/data/website(/.*)? all files system_u:object_r:httpd_sys_content_t:s0
The golden rule: Always use semanage fcontext followed by restorecon. Never use chcon in production.
audit2allow: Generate Policy from Denials
When you have a denial that cannot be fixed with a context change or a boolean β perhaps your custom application is doing something the policy does not cover β audit2allow can generate a custom policy module from the denial logs.
# Generate a policy module from recent denials
$ ausearch -m avc -ts recent | audit2allow -M myapp_custom
******************** IMPORTANT ***********************
To make this policy package active, execute:
semodule -i myapp_custom.pp
# Review what the policy does before applying
$ cat myapp_custom.te
module myapp_custom 1.0;
require {
type httpd_t;
type var_t;
class dir read;
}
#============= httpd_t ==============
allow httpd_t var_t:dir read;
# Apply the module
$ semodule -i myapp_custom.pp
# Verify it loaded
$ semodule -l | grep myapp
myapp_custom 1.0
Important warning: Always review the generated .te file before applying. audit2allow will generate policy that allows exactly what was denied β but it does not know if what was denied is something that should be allowed from a security perspective. If the denial is because nginx is trying to read /etc/shadow, you do not want to allow that.
semanage boolean: Toggle Features Without Writing Policy
SELinux booleans are pre-built switches in the policy that toggle common behaviors on and off. This is the easiest way to enable functionality that SELinux disables by default.
# List all booleans and their current state
$ getsebool -a | head -20
antivirus_can_scan_system --> off
antivirus_use_jit --> off
authlogin_nsswitch_use_ldap --> off
authlogin_radius --> off
cvs_read_shadow --> off
httpd_can_connect_ftp --> off
httpd_can_network_connect --> off
httpd_can_network_connect_db --> on
httpd_can_sendmail --> off
httpd_enable_cgi --> on
httpd_serve_cobbler_files --> off
httpd_use_nfs --> off
# Allow Apache to connect to the network (needed for reverse proxying)
$ setsebool -P httpd_can_network_connect on
# Allow Apache to connect to a database
$ setsebool -P httpd_can_network_connect_db on
# Allow Apache to read user home directories
$ setsebool -P httpd_read_user_content on
# Search for relevant booleans by keyword
$ semanage boolean -l | grep httpd | grep nfs
httpd_use_nfs (off , off) Allow httpd to use nfs
The -P flag makes the change permanent. Without it, the change reverts on reboot.
Real Scenario: nginx Serving Files From /data
Let us walk through a complete real-world fix. You have set up nginx to serve static files from /data/website, but it returns 403 Forbidden despite correct Linux permissions.
# Step 1: Confirm SELinux is the problem by checking denials
$ ausearch -m avc -c nginx -ts today
----
time->Fri Mar 6 10:45:12 2026
type=AVC msg=audit(1741255512.789:512): avc: denied { getattr } for
pid=3890 comm="nginx" path="/data/website/index.html"
scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:var_t:s0 tclass=file permissive=0
# Step 2: Check the current context of the directory
$ ls -Zd /data/website/
unconfined_u:object_r:var_t:s0 /data/website/
# Step 3: Set the correct permanent context rule
$ semanage fcontext -a -t httpd_sys_content_t "/data(/.*)?"
# Step 4: Apply the rule to all existing files
$ restorecon -Rv /data/
Relabeled /data from unconfined_u:object_r:var_t:s0 to system_u:object_r:httpd_sys_content_t:s0
Relabeled /data/website from unconfined_u:object_r:var_t:s0 to system_u:object_r:httpd_sys_content_t:s0
Relabeled /data/website/index.html from unconfined_u:object_r:var_t:s0 to system_u:object_r:httpd_sys_content_t:s0
# Step 5: Verify the context is correct
$ ls -Zd /data/website/
system_u:object_r:httpd_sys_content_t:s0 /data/website/
# Step 6: Test β nginx should now serve files correctly
$ curl -I http://localhost/
HTTP/1.1 200 OK
No setenforce. No disabling SELinux. The problem is fixed correctly and the fix will survive every reboot and every system relabel for the life of the server.
Common Beginner Mistakes on RHEL
- Running setenforce 0 as a permanent fix: This only lasts until reboot. Worse, it gives you false confidence that the problem is gone when it will return.
- Setting SELINUX=disabled and forgetting: Your system is now completely unprotected at the MAC layer. This is the most common finding in Linux security audits of RHEL systems.
- Using chcon in production: Context changes via chcon are wiped by restorecon. Always use semanage fcontext + restorecon.
- Not installing setroubleshoot-server: The sealert tool makes debugging so much easier. Install it on every RHEL system you manage.
- Applying audit2allow output blindly: Always read the generated .te policy file. If it is allowing something dangerous, the fix is wrong.
- Forgetting to check booleans: Many common services have booleans that enable specific behaviors. Check getsebool -a | grep <service> before writing custom policy.
Conclusion
SELinux is not your enemy. It is a mandatory access control system that enforces the principle of least privilege at the kernel level β and it has stopped real-world exploits from becoming system compromises. The frustration administrators feel with SELinux is almost always a symptom of not knowing how to read the audit logs and apply the right fix.
The workflow is simple: check ausearch or sealert to find the denial, determine whether it is a context problem (fix with semanage fcontext + restorecon), a port problem (fix with semanage port), or a behavior toggle (fix with setsebool), and only reach for audit2allow when the other tools cannot solve it. Keep SELinux in enforcing mode and learn to work with it rather than around it.
Was this article helpful?
About Ramesh Sundararamaiah
Red Hat Certified Architect
Expert in Linux system administration, DevOps automation, and cloud infrastructure. Specializing in Red Hat Enterprise Linux, CentOS, Ubuntu, Docker, Ansible, and enterprise IT solutions.