SELinux Basics
- What SELinux is
- Modes: enforcing, permissive, disabled
- Contexts — labels on every file and process
- Checking SELinux status
- Reading denial messages in audit.log
- audit2why — explain a denial
- audit2allow — generate a policy
- Booleans — pre-built policy switches
- Fixing file context mismatches
- Custom port labels
- Common SELinux scenarios
- Managing SELinux with Ansible
- Per-domain permissive mode
- restorecon dry-run (-n)
What SELinux is
SELinux (Security-Enhanced Linux) is a mandatory access control (MAC) system built into the Linux kernel. It enforces policy rules that determine what files, network ports, and system resources each process is allowed to access.
DAC (the standard Unix permission system — user/group/other + rwx) checks who you are. SELinux checks what you are allowed to do as this type of process. Even if you are root, SELinux can deny a privileged process from accessing a file or binding to a port if the policy does not allow it.
On RHEL/CentOS/Rocky Linux, SELinux is enabled and set to enforcing by default. Understanding it is not optional if you manage these systems.
Modes: enforcing, permissive, disabled
- Enforcing — SELinux is active and denying policy violations. This is the correct mode.
- Permissive — SELinux logs violations but does not block them. Useful for debugging — use temporarily, not permanently.
- Disabled — SELinux is completely off. Avoid. Requires reboot to re-enable (relabelling is needed).
# Check current mode
getenforce
# Check mode and policy details
sestatus
# Temporarily switch to permissive (reverts on reboot)
setenforce 0 # permissive
setenforce 1 # back to enforcing
setenforce only changes the mode for the current session. To make it permanent, edit /etc/selinux/config and set SELINUX=enforcing. Changing from disabled requires a reboot and full filesystem relabel.
Contexts — labels on every file and process
Every file, directory, process, and network socket has a SELinux context label in the format:
user:role:type:level
The type field is what matters most for day-to-day work. For example:
# View file context
ls -Z /var/www/html/
# system_u:object_r:httpd_sys_content_t:s0 index.html
# View process context
ps -eZ | grep httpd
# system_u:system_r:httpd_t:s0 httpd
# View your own context
id -Z
# unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
Apache (httpd_t) is only allowed to read files labelled httpd_sys_content_t. If you put a file in a non-standard location without relabelling it, Apache cannot read it even if Unix permissions are wide open.
Checking SELinux status
getenforce # Enforcing / Permissive / Disabled
sestatus # detailed status including policy name
cat /etc/selinux/config # permanent mode setting
Reading denial messages in audit.log
When SELinux denies something, it logs to /var/log/audit/audit.log.
# View recent denials
grep "type=AVC" /var/log/audit/audit.log | tail -20
# A typical denial:
type=AVC msg=audit(1712345678.123:456): avc: denied { read } for
pid=12345 comm="nginx" name="app.conf"
scontext=system_u:system_r:httpd_t:s0
tcontext=system_u:object_r:admin_home_t:s0
tclass=file permissive=0
Reading this denial:
- denied { read } — the read operation was denied
- comm="nginx" — the process that was denied
- name="app.conf" — the file it tried to access
- scontext=httpd_t — the context of the nginx process (source)
- tcontext=admin_home_t — the context of the file (target)
The problem: nginx (httpd_t) tried to read a file labelled admin_home_t. That is not allowed. The fix: relabel the file to httpd_sys_content_t.
audit2why — explain a denial
# Explain all recent denials
ausearch -m avc -ts recent | audit2why
# Example output:
# type=AVC msg=audit(...)...
# Was caused by:
# Missing type enforcement (TE) allow rule.
# Allow nginx to read files of type admin_home_t
audit2why reads the denial log and explains in plain English why the denial happened and what policy change would fix it.
audit2allow — generate a policy
# Generate a custom policy module from recent denials
ausearch -m avc -ts recent | audit2allow -M mymodule
# This creates two files:
# mymodule.te — the policy in text form
# mymodule.pp — the compiled policy module
# Install the policy module
semodule -i mymodule.pp
# Verify it is loaded
semodule -l | grep mymodule
mymodule.te to understand what you are permitting. Generating a policy that allows everything is not an improvement over disabling SELinux — be specific.
Booleans — pre-built policy switches
Booleans are on/off switches in the SELinux policy for common use cases. They are the right first tool — use them before writing custom policies.
# List all booleans
getsebool -a
# Search for relevant booleans
getsebool -a | grep nginx
getsebool -a | grep httpd
# Check a specific boolean
getsebool httpd_can_network_connect
# Set a boolean (current session only)
setsebool httpd_can_network_connect on
# Set permanently (persists across reboots)
setsebool -P httpd_can_network_connect on
Common booleans:
httpd_can_network_connect— allow Apache/Nginx to make outbound network connections (needed for reverse proxy)httpd_can_network_connect_db— allow Apache to connect to databaseshttpd_use_nfs— allow Apache to serve files from NFSnis_enabled— allow NIS (needed for SSSD with some configs)postfix_local_write_mail_spool— allow postfix to write to mail spool
Fixing file context mismatches
The most common SELinux problem: a file is in a non-standard location or was created without inheriting the right context.
# View current context on a file
ls -Z /opt/myapp/config.conf
# unconfined_u:object_r:default_t:s0 config.conf ← wrong label
# Check what label SELinux expects for that path
matchpathcon /opt/myapp/config.conf
# Set the correct context manually
chcon -t httpd_sys_content_t /opt/myapp/config.conf
# Better: set the default label policy for that path
semanage fcontext -a -t httpd_sys_content_t "/opt/myapp(/.*)?"
# Apply the policy to existing files
restorecon -Rv /opt/myapp/
chcon is a temporary label change and is lost on a relabel. semanage fcontext + restorecon is the permanent fix — it registers the rule in the policy and applies it.
Custom port labels
Services can only listen on ports that their context allows. If you configure nginx on port 8443 but the policy only allows httpd on 443/80, SELinux will block it.
# List port labels
semanage port -l
# List just http-related ports
semanage port -l | grep http
# Add a new port for nginx/apache
semanage port -a -t http_port_t -p tcp 8443
# Verify it was added
semanage port -l | grep 8443
Common SELinux scenarios
Nginx/Apache cannot serve files from a custom directory
semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"
restorecon -Rv /srv/www/
Nginx cannot connect to a backend (reverse proxy)
setsebool -P httpd_can_network_connect on
Postfix cannot connect to SASL auth
setsebool -P postfix_local_write_mail_spool on
# or check: getsebool -a | grep postfix
Service cannot write to a log directory
# Check what the log dir context is
ls -dZ /var/log/myapp/
# Apply correct label
semanage fcontext -a -t var_log_t "/var/log/myapp(/.*)?"
restorecon -Rv /var/log/myapp/
Managing SELinux with Ansible
---
# Set SELinux to enforcing
- name: Set SELinux to enforcing
ansible.posix.selinux:
policy: targeted
state: enforcing
# Set a boolean
- name: Allow nginx to connect to backends
ansible.posix.seboolean:
name: httpd_can_network_connect
state: true
persistent: true
# Set a file context
- name: Label custom web dir
community.general.sefcontext:
target: '/srv/www(/.*)?'
setype: httpd_sys_content_t
state: present
notify: Restore contexts
# Handler to apply the label
- name: Restore contexts
ansible.builtin.command: restorecon -Rv /srv/www/
changed_when: false
Per-domain permissive mode
Instead of disabling SELinux system-wide with setenforce 0 (which removes protection from everything), you can put a single process domain into permissive mode. This generates audit denials in the log without blocking the process — you can debug one service without disabling SELinux globally.
# Put the httpd_t domain into permissive mode
semanage permissive -a httpd_t
# Verify it is in the permissive list
semanage permissive -l
# The domain now logs denials (as AVC) but does not block
# Watch the audit log while testing
tail -f /var/log/audit/audit.log | grep "httpd_t"
# Revert — remove the domain from the permissive list
semanage permissive -d httpd_t
Per-domain permissive mode persists across reboots (stored in the SELinux policy). Always remove it with semanage permissive -d domain_t once debugging is complete. If you forget, semanage permissive -l shows all permanently permissive domains.
httpd_t (nginx/Apache), mysqld_t (MySQL/MariaDB), postfix_smtpd_t (Postfix), sshd_t (SSH). Check the exact domain with ps auxZ | grep processname.
restorecon dry-run (-n)
Before running restorecon on a large directory tree, use -n to preview what labels would change without actually changing anything.
# Dry-run: show what would be relabeled (no changes made)
restorecon -n -Rv /srv/www/
# If output looks correct, run without -n to apply
restorecon -Rv /srv/www/
# Combine with -F to force relabel even if the label is already correct
restorecon -n -FRv /srv/www/
# Dry-run on a single file
restorecon -n -v /etc/nginx/conf.d/app.conf
Typical output of the dry-run:
Would relabel /srv/www/app/config.conf from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
Would relabel /srv/www/uploads/ from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:httpd_sys_rw_content_t:s0
Use the dry-run first whenever running restorecon on directories that contain application data. Unexpected relabeling can break running services if the labels change to something the process domain cannot access.