Skip to content

SELinux Essentials for Sysadmins

SELinux (Security-Enhanced Linux) is a Mandatory Access Control (MAC) system baked into the kernel. It is the single most common reason a "correctly configured" service still refuses to work on RHEL — and the single most common thing beginners disable instead of learning. This page teaches you to keep it on and work with it.

Tested on

AlmaLinux 9.4 with the default targeted policy. Commands also apply to RHEL 9, Rocky Linux 9, and CentOS Stream 9.

What SELinux is and why RHEL keeps it on

Standard Linux permissions (the rwx bits you set with chmod) are Discretionary Access Control (DAC): the owner of a file decides who may access it. That is fine until a process is compromised — a hijacked web server running as apache can read anything apache is allowed to read.

SELinux adds a second, independent layer of Mandatory Access Control. Every process and every file carries a security context (a label). The kernel enforces a central policy that says, for example, "a process labelled httpd_t may read files labelled httpd_sys_content_t and nothing else." Even if an attacker takes over Apache, the policy boxes them in. This is called type enforcement and it is the heart of the targeted policy that RHEL ships.

DAC is checked first

SELinux only ever restricts — it never grants access that DAC denied. A request must pass both the traditional permission check and the SELinux policy.

Red Hat keeps SELinux enforcing by default because it has repeatedly contained real-world exploits (Shellshock, various PHP and FTP escapes) that would otherwise have led to full compromise.

Debian/Ubuntu use AppArmor

Debian and Ubuntu ship AppArmor instead, which confines programs by path using per-application profiles in /etc/apparmor.d/. The concepts (mandatory confinement, complain vs. enforce modes) are similar, but the commands (aa-status, aa-complain, aa-enforce) and configuration are completely different.

The three modes

Mode What it does
Enforcing Policy is applied; violations are blocked and logged. This is the production default.
Permissive Policy is not applied; violations are allowed but still logged. Ideal for troubleshooting.
Disabled SELinux is off entirely; no labelling, no enforcement. Avoid.

Checking and changing the mode

# Show the current runtime mode
getenforce
Enforcing
# Show mode plus policy details
sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/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

Switch between Enforcing and Permissive at runtime — this is temporary and resets on reboot:

sudo setenforce 0    # switch to Permissive
sudo setenforce 1    # switch back to Enforcing

To make the mode persistent, edit /etc/selinux/config:

# /etc/selinux/config
SELINUX=enforcing
# SELINUXTYPE can be targeted (default) or mls
SELINUXTYPE=targeted

Do not set SELINUX=disabled

Use permissive for troubleshooting, never disabled. Setting disabled stops the kernel from labelling files at all. When you later switch back to enforcing, the entire filesystem is mislabelled and the system must do a slow, sometimes failure-prone autorelabel on the next boot. Permissive keeps labels current while letting everything through, so you can flip straight back to enforcing.

If you must relabel after a disabled period:

# Force a full relabel on next reboot
sudo touch /.autorelabel
sudo reboot

Security contexts

A context has four fields: user:role:type:level.

ls -Z /var/www/html/index.html
unconfined_u:object_r:httpd_sys_content_t:s0 /var/www/html/index.html
  • unconfined_u — SELinux user (maps from the Linux user; most logins map to unconfined_u).
  • object_rrole (files almost always use object_r; roles matter mostly for processes).
  • httpd_sys_content_t — the type. This is the field you will care about 95% of the time.
  • s0 — the MLS/MCS level (sensitivity), used by container isolation and MLS policy.

You can inspect contexts of processes and yourself too:

ps -eZ | grep httpd        # context of running processes
id -Z                      # the context of your current shell
system_u:system_r:httpd_t:s0   1234 ?  00:00:01 httpd
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

Type enforcement boils down to: a process of type httpd_t is allowed to act on files of type httpd_sys_content_t. Get the file type wrong and Apache gets "Permission denied" even though ls -l shows perfect rwx bits.

Common types you will meet

Type Applies to
httpd_sys_content_t Read-only web content (/var/www/html)
httpd_sys_rw_content_t Web directories the server must write to (uploads, caches)
httpd_log_t Apache/nginx log files
ssh_home_t A user's ~/.ssh directory and authorized_keys
ssh_port_t The port(s) sshd listens on
var_log_t General log files under /var/log
public_content_t Content shared read-only across FTP/HTTP/Samba

Booleans — flipping policy switches

Booleans are pre-built on/off tunables that let you adjust policy without writing any. List them:

getsebool -a | grep httpd
httpd_can_network_connect --> off
httpd_can_network_connect_db --> off
httpd_can_sendmail --> off
httpd_enable_homedirs --> off
httpd_use_nfs --> off

A classic case: your PHP/Apache app needs to call an external API or connect to a remote database, but SELinux blocks outbound connections from httpd_t by default. Turn the boolean on:

# -P writes the change to disk so it survives reboots
sudo setsebool -P httpd_can_network_connect on

Always use -P for permanent changes

setsebool httpd_can_network_connect on (without -P) only changes the running value and is lost on reboot — leading to "it broke again after a reboot" mysteries. Use -P for anything you want to keep.

Verify:

getsebool httpd_can_network_connect
httpd_can_network_connect --> on

Fixing labels

When files end up with the wrong type — the most common cause being content created or moved into a directory from somewhere else — you fix the labels rather than disabling SELinux.

restorecon — reset to the policy default

restorecon looks up what the type should be (from the policy's file-context rules) and applies it. This is the right tool most of the time:

# Recursively restore the correct contexts, -v shows what changed
sudo restorecon -Rv /var/www/html
Relabeled /var/www/html/index.html from unconfined_u:object_r:user_home_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0

chcon — change a context (temporary)

chcon sets a label directly. Use it for quick tests, but know that a future restorecon or filesystem relabel will overwrite it because it is not recorded in the policy:

sudo chcon -t httpd_sys_content_t /var/www/html/extra.html

semanage fcontext — make a custom context permanent

If you serve web content from a non-standard directory (say /srv/web), tell the policy that this path should be httpd_sys_content_t, then apply it:

# Add a permanent rule: everything under /srv/web is web content
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/web(/.*)?"

# Apply the new rule to existing files
sudo restorecon -Rv /srv/web

Now even a full relabel will keep /srv/web correctly typed.

semanage lives in policycoreutils-python-utils

If semanage is missing: sudo dnf install -y policycoreutils-python-utils.

Registering non-standard ports

Type enforcement covers ports too. If you move SSH to port 2222 or run Apache on 8443, SELinux must be told, or the service fails to bind.

# See which ports the policy currently allows for httpd
sudo semanage port -l | grep http_port_t
http_port_t  tcp  80, 81, 443, 488, 8008, 8009, 8443, 9000
# Allow Apache to listen on a new port
sudo semanage port -a -t http_port_t -p tcp 8081

# Move sshd to a non-standard port (label it ssh_port_t)
sudo semanage port -a -t ssh_port_t -p tcp 2222

If a rule already exists for that port type, use -m (modify) instead of -a (add).

Troubleshooting denials

When something is mysteriously blocked, SELinux has written an AVC (Access Vector Cache) denial to the audit log. Read it.

# Show AVC denials from the recent past
sudo ausearch -m avc -ts recent
type=AVC msg=audit(1718000000.123:456): avc:  denied  { read } for  pid=1234
  comm="httpd" name="report.html" dev="dm-0" ino=98765
  scontext=system_u:system_r:httpd_t:s0
  tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

Reading it: a process httpd (scontext type httpd_t) was denied read on a file labelled user_home_t (tcontext). The fix is obvious — the file has the wrong type, so restorecon it.

sealert (from setroubleshoot-server) turns raw denials into human-readable advice with suggested fixes:

sudo dnf install -y setroubleshoot-server
sudo sealert -a /var/log/audit/audit.log
SELinux is preventing httpd from read access on the file report.html.

*****  Plugin restorecon (94.8 confidence) suggests  ************************
If you want to fix the label, you can run:
# /sbin/restorecon -v /var/www/html/report.html

audit2allow — generate policy (with care)

When no boolean or relabel fits — for genuinely new, legitimate behaviour — audit2allow can build a custom policy module from the denials:

# Read denials and propose a human-readable policy
sudo ausearch -m avc -ts recent | audit2allow
#============= httpd_t ==============
allow httpd_t myapp_socket_t:sock_file write;

To compile and install it as a module:

sudo ausearch -m avc -ts recent | audit2allow -M myhttpd
sudo semodule -i myhttpd.pp

Understand before you allow

audit2allow will happily write a rule to permit whatever was denied — including the access an attacker triggered. Never pipe it blindly. First confirm the denial is from legitimate activity, prefer a boolean or restorecon if one applies, and only generate a module for behaviour you have verified is correct. A bad audit2allow module can silently hand back the protection SELinux was giving you.

Verify your work

# 1. SELinux is enforcing
getenforce
# Expect: Enforcing

# 2. Web content carries the right type
ls -Z /var/www/html/index.html
# Expect: ...:httpd_sys_content_t:s0 ...

# 3. Your network boolean is on AND persistent
getsebool httpd_can_network_connect
# Expect: httpd_can_network_connect --> on

# 4. Custom port is registered
sudo semanage port -l | grep -E 'http_port_t|ssh_port_t'
# Expect to see your 8081 / 2222 entries

# 5. No fresh denials after exercising the service
sudo ausearch -m avc -ts recent
# Expect: <no matches>

Summary

  • SELinux is Mandatory Access Control layered on top of normal Unix permissions; it confines processes by their type (type enforcement) and is why a misconfigured-looking service often just has a wrong file label.
  • Use the three modes wisely: keep enforcing in production, drop to permissive to troubleshoot, and never set disabled.
  • Read contexts with ls -Z, ps -Z, id -Z; the type field is what matters most.
  • Adjust behaviour with booleans (setsebool -P), fix labels with restorecon, make custom paths permanent with semanage fcontext, and register custom ports with semanage port.
  • Diagnose blocks by reading AVC denials (ausearch -m avc -ts recent, sealert); use audit2allow only after you understand the denial.
  • Debian/Ubuntu use AppArmor, a path-based MAC system with its own tooling.

Next, layer dynamic IP-based protection with Fail2ban, work through the Server Hardening Checklist, and lock down the perimeter with Firewalls.

Test yourself