SELinux Modes and How to Switch Between Them
SELinux runs in one of three modes: Enforcing, Permissive, or Disabled. Enforcing blocks policy violations. Permissive logs them without blocking - useful for debugging. Disabled turns SELinux off entirely, which requires a reboot and leaves the filesystem without labels.
Check the current runtime mode with `getenforce`. For more detail, `sestatus` shows the mode, the loaded policy name, and the MLS status. On a fresh RHEL 9 install you will typically see policy `targeted` and mode `Enforcing`.
Switching between Enforcing and Permissive at runtime requires no reboot:
``` setenforce 0 # switch to Permissive setenforce 1 # switch back to Enforcing ```
The persistent setting lives in `/etc/selinux/config`. The `SELINUX=` line controls what mode the system boots into. Never set it to `disabled` on a production server unless you have a documented reason and a plan to relabel the entire filesystem on re-enable. When you re-enable after a disabled state, you must trigger a full relabel either by touching `/.autorelabel` or running `fixfiles -F onboot`, then reboot. On a server with a 200 GB filesystem, that relabel can add 10-15 minutes to boot time.
# Check current state
getenforce
sestatus
# Runtime toggle (no reboot needed)
setenforce 0
setenforce 1
# Persistent config
cat /etc/selinux/config
# SELINUX=enforcing
# SELINUXTYPE=targeted
Understanding Security Contexts
Every object in SELinux has a security context in the format `user:role:type:level`. In practice, the `type` field is what most targeted-policy work revolves around. Run `ls -Z` on any file to see its context.
For processes, use `ps auxZ`. For your current shell session, run `id -Z`. A typical file context on a web document root looks like `system_u:object_r:httpd_sys_content_t:s0`. The `httpd_sys_content_t` type is what allows Apache to read those files.
Context mismatches are the most common cause of SELinux-related service failures. If you copy a file with `cp` instead of `mv`, the destination inherits the source directory context. If you move a config file from your home directory into `/etc/`, it keeps `user_home_t` instead of getting the appropriate `etc_t` or service-specific type. The service then gets denied read access and you see nothing in the application log.
The `stat` command with `--printf='%C\n'` also prints the SELinux context, which is handy in scripts where you need just the context string without the full `ls -Z` output.
# Show context on files
ls -Z /var/www/html/
# Show context on running processes
ps auxZ | grep httpd
# Show your current process context
id -Z
# Context in scripts
stat --printf='%C\n' /etc/nginx/nginx.conf
Reading the Audit Log: Finding What SELinux Blocked
SELinux logs denials to `/var/log/audit/audit.log` via the kernel audit subsystem. A raw denial looks like this:
``` type=AVC msg=audit(1718200000.123:456): avc: denied { read } for pid=1234 comm="nginx" name="app.sock" dev="tmpfs" ino=9999 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:var_t:s0 tclass=sock_file permissive=0 ```
Reading raw AVC messages is workable but slow. The `audit2why` tool translates them into plain English. Pipe the relevant log lines through it:
``` grep 'denied' /var/log/audit/audit.log | audit2why ```
For ongoing monitoring, `ausearch` is more surgical. It can filter by time, by process name, or by SELinux context. The `-m AVC` flag limits results to SELinux denials specifically.
In our experience on busy application servers, the audit log fills up fast. Check `auditd` rotation settings in `/etc/audit/auditd.conf` - the defaults cap the log at 8MB with 5 rotations, which is often not enough for a debugging session spanning several hours. We set `max_log_file = 50` and `num_logs = 10` on test servers during policy development.
If `setroubleshootd` is installed, it writes friendlier messages to `/var/log/messages` with suggested fixes including the exact `setsebool` or `semanage` commands to run. On servers without a GUI, install `setroubleshoot-server` to get the `sealert` CLI tool.
# Search for recent AVC denials
ausearch -m AVC -ts recent
# Human-readable explanation
grep 'denied' /var/log/audit/audit.log | audit2why
# Use sealert on a specific audit ID
sealert -a /var/log/audit/audit.log
# Filter by process name
ausearch -m AVC -c nginx
Fixing File Contexts with restorecon and semanage
Two tools handle file context correction. `restorecon` resets a file or directory back to the context defined in the policy database. `semanage fcontext` adds or modifies the policy database itself, so future files in that path get the right label automatically.
The workflow is: use `semanage fcontext` to define the rule, then `restorecon` to apply it immediately without waiting for a relabel.
For a custom application serving files from `/opt/myapp/public`, the default policy has no rule for that path. Assign it the same type as a standard web root:
``` semanage fcontext -a -t httpd_sys_content_t '/opt/myapp/public(/.*)?' restorecon -Rv /opt/myapp/public ```
The `-R` flag recurses into subdirectories. The `-v` flag prints each change. On a directory with thousands of files, pipe through `tee` to capture the output.
To verify what the policy database says about a path before applying it, use `matchpathcon`:
``` matchpathcon /opt/myapp/public/index.html ```
If you need to check what contexts are defined for a particular type across all paths, query the fcontext database:
``` semanage fcontext -l | grep httpd_sys_content_t ```
On our test server running Rocky Linux 9.4, the `semanage fcontext` change persists across reboots and reinstalls of the package because it writes to the local policy store in `/etc/selinux/targeted/contexts/files/`. The `restorecon` step is a one-time application of that stored rule.
# Define context rule for a custom path
semanage fcontext -a -t httpd_sys_content_t '/opt/myapp/public(/.*)?'
# Apply it immediately
restorecon -Rv /opt/myapp/public
# Verify expected context
matchpathcon /opt/myapp/public/index.html
# List all fcontext rules for a type
semanage fcontext -l | grep httpd_sys_content_t
# Check current vs expected context on a file
ls -Z /opt/myapp/public/index.html
SELinux Booleans: Adjusting Policy Without Writing Modules
Booleans are named on/off switches that control optional behaviors in the loaded policy. They exist so you can enable common but non-default configurations without writing custom policy modules. On RHEL 9, there are over 300 booleans in the targeted policy.
List all booleans with `getsebool -a`. Filter with `grep` for your service. For example, to find all booleans related to HTTPD:
``` getsebool -a | grep httpd ```
Common practical examples: `httpd_can_network_connect` allows Apache to make outgoing TCP connections, which you need if Apache proxies to a backend application. `httpd_use_nfs` allows Apache to serve files from NFS mounts. `ftpd_anon_write` allows anonymous FTP uploads.
Toggle a boolean at runtime with `setsebool`. Add `-P` to make it persistent across reboots by writing it to the policy store:
``` setsebool -P httpd_can_network_connect 1 ```
Without `-P`, the change reverts on reboot. We have seen this catch out engineers who test a fix in Permissive mode, re-enable Enforcing, set a boolean without `-P`, and then wonder why the service breaks again after the next maintenance window reboot.
To document which booleans are active and why, `semanage boolean -l` shows the current value, the default value, and a description string. Redirect that to a file and store it in your infrastructure documentation repo - it is far more useful than trying to reconstruct policy decisions six months later.
# List booleans for a service
getsebool -a | grep httpd
# Enable a boolean persistently
setsebool -P httpd_can_network_connect 1
setsebool -P httpd_use_nfs 1
# Show boolean descriptions and current state
semanage boolean -l | grep httpd_can_network
# Check a single boolean
getsebool httpd_can_network_connect
Writing Local Policy Modules with audit2allow
When booleans and context fixes do not cover your use case, you need a custom policy module. The `audit2allow` tool generates policy rules directly from AVC denial messages. This is not a substitute for understanding what you are allowing - always review the generated rules before loading them.
The standard workflow: reproduce the denial with SELinux in Permissive mode so the service runs but denials are logged, then capture those denials and generate a module.
Step 1: Put the relevant process into Permissive mode without disabling SELinux globally. `semanage permissive` adds a type to the permissive domain list:
``` semanage permissive -a httpd_t ```
Step 2: Trigger the application behavior that was failing. Run through your full functional test so all denials get logged.
Step 3: Generate and review the module:
``` ausearch -m AVC -c httpd | audit2allow -M myapp_httpd ```
This produces `myapp_httpd.te` (the type enforcement source) and `myapp_httpd.pp` (the compiled binary module). Read `myapp_httpd.te` carefully. If you see something like `allow httpd_t shadow_t:file { read }`, that is a red flag - you are allowing Apache to read `/etc/shadow`, which almost certainly means something else is misconfigured.
Step 4: Load the module:
``` semodule -i myapp_httpd.pp ```
Step 5: Remove the permissive exception:
``` semanage permissive -d httpd_t ```
Step 6: Verify the service works with SELinux fully enforcing.
Store your `.te` files in version control. If you are running automated infrastructure with tools like those catalogued at taskbotshub.ai, you can integrate `semodule -i` into your configuration management playbooks so custom policy modules deploy consistently across new nodes without manual intervention.
To list loaded modules: `semodule -l`. To remove a module: `semodule -r myapp_httpd`.
# Add httpd_t to permissive domain list
semanage permissive -a httpd_t
# Generate module from AVC denials
ausearch -m AVC -c httpd | audit2allow -M myapp_httpd
# Review the generated rules before loading
cat myapp_httpd.te
# Load the compiled module
semodule -i myapp_httpd.pp
# Remove the permissive exception
semanage permissive -d httpd_t
# Verify module is loaded
semodule -l | grep myapp
# Remove a module if needed
semodule -r myapp_httpd
SELinux Port Labeling for Non-Standard Ports
If your service listens on a non-standard port, SELinux will deny the bind unless that port is labeled with the correct type. Apache listening on port 8443 instead of 443 is a common example.
Check what ports are already labeled for a type:
``` semanage port -l | grep http ```
Output on RHEL 9 shows that `http_port_t` includes 80, 81, 443, 488, 8008, 8009, 8443, and 9000 by default. If your port is not in that list, add it:
``` semanage port -a -t http_port_t -p tcp 8080 ```
To modify an existing labeled port to a different type:
``` semanage port -m -t http_port_t -p tcp 8080 ```
Port labeling errors appear in the audit log as denials on `name_bind` permission for the `tcp_socket` or `udp_socket` class. Run `ausearch -m AVC | grep name_bind` to isolate these specifically.
On our test server, we ran a custom Go application on port 9001. The port was not labeled for any relevant type. The process context was a custom `myapp_t` we had defined. We added the port label with `semanage port -a -t myapp_port_t -p tcp 9001` after defining `myapp_port_t` in our custom policy module. If you are using the `httpd_t` context for your application through a wrapper or FastCGI setup, you can simply add the port to `http_port_t` instead of defining a new port type.
# List ports for http service types
semanage port -l | grep http
# Add port 8080 to http_port_t
semanage port -a -t http_port_t -p tcp 8080
# Modify an existing port label
semanage port -m -t http_port_t -p tcp 8080
# Find name_bind denials specifically
ausearch -m AVC | grep name_bind
# List all labeled ports
semanage port -l
Practical Debugging Workflow: From Broken Service to Running Policy
When a service fails after enabling SELinux or deploying to a new server, follow this sequence. Do not disable SELinux as a first step - that masks the actual problem and leaves the system unprotected.
1. Check if SELinux is the cause. Switch to Permissive mode, restart the service. If it works, SELinux was blocking it. If it still fails, the problem is elsewhere.
2. Switch back to Enforcing and reproduce the failure with `ausearch -m AVC -ts recent` running in a second terminal or in a `watch` loop:
``` watch -n 2 'ausearch -m AVC -ts recent 2>/dev/null | tail -20' ```
3. Identify the denial type. Is it a file context mismatch? Use `restorecon`. Is it a boolean that covers the use case? Use `setsebool -P`. Is it a port label issue? Use `semanage port`. Only if none of those apply should you move to `audit2allow`.
4. If you use `audit2allow`, put the service domain into Permissive mode rather than the whole system. Capture all denials in one session to avoid partial policy modules that require multiple iterations.
5. After any fix, test in Enforcing mode with `setenforce 1` before considering the work done.
This workflow applies whether you are configuring a single server or templating a golden image. If your team automates server builds - and in 2026 most do, often with AI-assisted pipeline tools catalogued on sites like taskbotshub.ai - encoding SELinux policy steps into the provisioning sequence means every new node starts with the correct labels and modules already in place rather than relying on post-deployment fixups.
Keep a runbook entry for each service documenting: what contexts were set, what booleans were enabled, and what custom modules were loaded. The `semanage export` command dumps the full local policy customization to stdout, which is ideal for saving to a file in your config management repo:
``` semanage export > /root/selinux-local-policy-$(hostname)-$(date +%Y%m%d).cfg ```
# Watch for live AVC denials
watch -n 2 'ausearch -m AVC -ts recent 2>/dev/null | tail -20'
# Put a specific domain into permissive for debugging
semanage permissive -a nginx_t
# Export all local SELinux customizations
semanage export > /root/selinux-local-policy-$(hostname)-$(date +%Y%m%d).cfg
# Import on a new server
semanage import < selinux-local-policy-webserver-20260101.cfg
Common Mistakes and How to Avoid Them
The most common mistake is permanently disabling SELinux because fixing the policy seems harder than turning it off. This trades a 30-minute fix for a permanently degraded security posture. Every RHEL security benchmark, including CIS and DISA STIG, flags `SELINUX=disabled` as a finding.
The second most common mistake is setting everything to `unconfined_t`. Running a process as `unconfined_t` means SELinux effectively ignores it - you get none of the protection. If you find yourself doing this, write a proper custom policy instead.
The third mistake is over-relying on `audit2allow` without reading the output. The tool generates the minimum rules needed to allow what was denied - it does not evaluate whether that allowance is appropriate. We have seen generated policies that allowed `httpd_t` to write to `shadow_t` because someone had a misconfigured PHP script trying to read `/etc/shadow`. The correct fix was the PHP script, not a policy rule.
A subtler problem: using `cp -a` or `rsync -a` to restore files from backup copies the SELinux extended attributes along with the files. If the backup came from a system with different policy or different path expectations, the restored files may have wrong contexts. Always run `restorecon -Rv` on restored directories.
Finally, when troubleshooting with a team, someone will invariably set `setenforce 0` temporarily and forget to restore it. Add this to your monitoring: alert if `getenforce` returns `Permissive` on any production node. A one-line cron job or a Prometheus textfile collector exporting the SELinux mode as a metric costs five minutes to set up and has saved us from extended Permissive windows more than once.
# Simple monitor: alert if not Enforcing
# /etc/cron.d/selinux-check
*/5 * * * * root [ "$(getenforce)" != "Enforcing" ] && logger -t selinux "WARNING: SELinux is not Enforcing on $(hostname)"
# Check for unconfined processes
ps auxZ | grep unconfined_t | grep -v grep
# Verify contexts after rsync restore
rsync -aX /backup/etc/ /etc/
restorecon -Rv /etc/