Configuring SELinux on Rocky Linux: Don’t Disable It Before You Understand What It Does

CentOS tutorial - IT technology blog
CentOS tutorial - IT technology blog

2 AM and the setenforce 0 Trap

Back when CentOS 8 reached EOL, I had to migrate 5 servers to Rocky Linux within a week. On the first night, Nginx refused to bind to the new port, and PHP-FPM was throwing permission errors even though chown was set correctly. Hands shaking, eyes blurring, I typed setenforce 0 and everything just worked. Task done, went to sleep.

The next morning, I took a look and got a shock: the production servers were running with SELinux disabled. Not one server — all 5. And I knew that if left like that, it would be the first red flag in any security audit.

From that point on, I started asking myself: what is SELinux blocking, why, and how do you fix it without just turning it off? This post is the result — how I brought all 5 servers back to Enforcing mode without breaking anything else.

Three Ways to Handle SELinux — and Why Two of Them Don’t Belong on Production

Browse any forum or StackOverflow thread, and most sysadmins handle SELinux one of three ways:

Option 1: Disable SELinux entirely (SELINUX=disabled)

This is common in older tutorials, especially LAMP stack guides from 2018 and earlier. Edit /etc/selinux/config, reboot, done.

# /etc/selinux/config
SELINUX=disabled

The only upside: everything works immediately, no debugging needed. The downsides outweigh that significantly — you’ve killed an entire kernel-level security layer, and re-enabling it later requires relabeling the entire filesystem: my 150GB server took nearly 18 minutes just for that step.

Option 2: Permissive Mode — Logs but Doesn’t Block

SELinux still runs and checks policy, but instead of blocking, it just logs to the audit log. Think of it as a dry run — it catches violations but doesn’t act on them.

setenforce 0        # Temporary (lost after reboot)
getenforce          # Check status: will output "Permissive"

To make it persistent across reboots:

vi /etc/selinux/config
# SELINUX=permissive

Option 3: Enforcing — Real Security

This is the mode you actually want. Policy violations are blocked immediately — no warnings, no exceptions. It’s the default when you do a fresh Rocky Linux install.

setenforce 1
getenforce          # Output: Enforcing

Practical Analysis: Which Mode Fits Which Situation?

Mode Security Easy to Debug? When to Use
Disabled None Not needed Never on production
Permissive Doesn’t block Easiest Dev/staging, or while debugging
Enforcing Full Need to know audit log Production — the ultimate goal

If you’re running a real server, Disabled is the worst choice. Permissive is fine for test environments, but it’s not a long-term solution. Enforcing is where you need to be.

The Plan: Moving from Permissive → Enforcing in a Controlled Way

The approach I used when migrating those 5 servers was:

  1. Enable Permissive mode first
  2. Run services normally for a few days to let SELinux accumulate enough denials
  3. Use audit2allow to create custom policies
  4. Load the policies and switch to Enforcing

It takes a few extra days to let the logs accumulate, but in return you’re not fumbling in the dark — you know exactly which services need what permissions before flipping the switch.

Practical Implementation Guide

Step 1: Check the Current Status

sestatus
# Sample output:
# SELinux status:                 enabled
# SELinuxfs mount:                /sys/fs/selinux
# SELinux mount point:            /sys/fs/selinux
# Loaded policy name:             targeted
# Current mode:                   permissive
# Mode from config file:          enforcing
# Policy MLS status:              enabled
# Policy deny_unknown status:     allowed
# Memory protection checking:     actual (secure)
# Max kernel policy version:      33

Pay attention to the distinction between “Current mode” (runtime) and “Mode from config file” (after reboot). These two can differ — especially if you’ve just used setenforce without updating the config file.

Step 2: Read the AVC Denial Log

In Permissive mode, every violation gets logged to /var/log/audit/audit.log:

# View recent denials
ausearch -m avc -ts recent

# Or use raw grep
grep "avc:  denied" /var/log/audit/audit.log | tail -20

A typical AVC denial looks like this:

type=AVC msg=audit(1709712345.123:456): avc:  denied  { read } for  pid=12345 comm="nginx" \
  name="app.sock" dev="tmpfs" ino=67890 \
  scontext=system_u:system_r:httpd_t:s0 \
  tcontext=system_u:object_r:var_run_t:s0 tclass=sock_file permissive=1

Read it in order: which process (comm), what it’s doing (read), on which file (name), denied because the type doesn’t match.

Step 3: Analyze with sealert (More Human-Readable)

Reading audit logs by eye is exhausting. setroubleshoot-server parses them into plain English:

dnf install -y setroubleshoot-server

# Run against the audit log
sealert -a /var/log/audit/audit.log

sealert gives specific recommendations — usually the exact setsebool or semanage command you need to run, along with an explanation of why.

Step 4: Create a Custom Policy with audit2allow

When a service has non-standard behavior — for example, Nginx serving files from a non-standard directory — you need to create a custom policy:

# Create a policy module from the audit log
ausearch -m avc -ts recent | audit2allow -M my_nginx_custom

# Load the newly created policy
semodule -i my_nginx_custom.pp

# Verify it's loaded
semodule -l | grep my_nginx

The .te file is the text source, .pp is the compiled policy. Keep the .te file — if you need to modify the policy later, edit that file instead of starting from scratch.

Step 5: Use Booleans Instead of Creating New Policies

Many common scenarios already have booleans available — using them is faster and cleaner:

# Allow httpd to make outbound connections (API calls, proxy)
setsebool -P httpd_can_network_connect 1

# Allow httpd to read home directories
setsebool -P httpd_enable_homedirs 1

# Allow nginx/apache to bind to a non-standard port
semanage port -a -t http_port_t -p tcp 8080

# View all httpd-related booleans
getsebool -a | grep httpd

Step 6: Relabel File Context If Needed

When you copy files into a new directory or create files outside the standard path, the SELinux context is often wrong. Fix it like this:

# View current context
ls -Z /var/www/html/

# Assign the correct context for a web directory
semanage fcontext -a -t httpd_sys_content_t "/data/web(/.*)?";
restorecon -Rv /data/web/

# Or if fixing a single file
chcon -t httpd_sys_content_t /data/web/index.php

Step 7: Switch to Enforcing

No new denials in the log after a few days in Permissive mode? That’s your signal to switch:

# Temporarily (test immediately)
setenforce 1

# Monitor the log for 30 minutes
tail -f /var/log/audit/audit.log | grep denied

# If all looks good, update config to make it persistent
vi /etc/selinux/config
# SELINUX=enforcing

I usually open a second terminal tab running tail -f on audit.log right after switching to Enforcing. Wait ~30 minutes, test all the main app functionality. No new denials means you’re done.

Quick Reference: SELinux Commands to Know

# Status
sestatus
getenforce

# Change runtime mode
setenforce 0   # Permissive
setenforce 1   # Enforcing

# View context
ls -Z /path/to/file
ps auxZ | grep nginx

# Fix context
restorecon -Rv /path/     # Restore to policy default
chcon -t TYPE /file       # Temporary fix

# Port
semanage port -l | grep http
semanage port -a -t http_port_t -p tcp 8443

# Log
ausearch -m avc -ts recent
sealert -a /var/log/audit/audit.log

# Policy
audit2allow -M mypolicy < /var/log/audit/audit.log
semodule -i mypolicy.pp
semodule -l

Closing Thoughts

The trap with SELinux is that it’s easy to turn off and hard to turn back on correctly. After that migration experience, I set a personal rule: no server goes to production without SELinux in Enforcing mode. It costs an extra hour or two of upfront debugging, but what you get in return is a real security layer — not just security theater.

Rocky Linux preserves RHEL’s SELinux behavior. Every command above works identically on AlmaLinux or any other RHEL clone.

Share: