SELinux Basics

What SELinux is, how to read denials, and how to fix them correctly without disabling SELinux.

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.

Do not disable SELinux. It is a critical security layer. Disabling it is the wrong answer to every SELinux problem — it is a shortcut that leaves systems vulnerable. Learn to fix denials properly instead.

Modes: enforcing, permissive, disabled

# 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:

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
Review before installing. audit2allow creates a policy that allows the denial. Read 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:

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.

Common domains to debug: 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.