SELinux Explained
SELinux is the most powerful and most misunderstood security feature in the Linux ecosystem. It is not a replacement for file permissions — it is an additional enforcement layer that confines what every process on the system is allowed to do, independently of standard Unix permissions. When configured correctly, it means a compromised web server cannot read your database password files even if it runs as a user with access to them.
SELinux modes
# Check current SELinux status and mode
sestatus
getenforce # quick: outputs Enforcing, Permissive, or Disabled
# Set mode temporarily (survives until next reboot)
sudo setenforce 1 # Enforcing
sudo setenforce 0 # Permissive (logs but does not block)
# Set mode permanently (requires editing config file)
sudo nano /etc/selinux/config
# SELINUX=enforcing
# SELINUX=permissive
# SELINUX=disabled # requires reboot to take effect
The three modes:
- Enforcing: SELinux policy is active and denials are enforced. This is the production setting.
- Permissive: Denials are logged but not enforced. Use for troubleshooting or building new policies.
- Disabled: SELinux is completely off. Not recommended — re-enabling requires a full relabel of the filesystem on the next boot.
SELinux contexts
Every object in the system (files, processes, sockets, ports) has an SELinux context. Contexts have the format:
user:role:type:level
system_u:object_r:httpd_sys_content_t:s0
The type is what most administrators interact with day-to-day. Policy rules say “type A may access type B in way C.”
# Show file contexts
ls -Z /var/www/html/
ls -Z /etc/nginx/
ls -Z /home/colton/
# Show process contexts
ps -eZ | grep nginx
ps -eZ | grep sshd
ps -Z -p $$ # context of current shell
# Show context of your current session
id -Z
# Show port contexts
sudo semanage port -l | grep http
sudo semanage port -l | grep 22
Common types you will see
| Type | Meaning |
|---|---|
httpd_sys_content_t | Web server content (read by httpd) |
httpd_sys_rw_content_t | Web content that httpd can write |
httpd_t | The Apache/nginx process type |
sshd_t | The SSH daemon process type |
postgresql_db_t | PostgreSQL database files |
var_t | Generic /var files |
user_home_t | Files in user home directories |
admin_home_t | Files in /root |
Finding and reading denials
SELinux logs all denials to the audit log. When something breaks and you suspect SELinux, look here first.
# View recent AVC (Access Vector Cache) denials
sudo ausearch -m avc -ts recent
# More detail
sudo ausearch -m avc -ts recent -i # -i interprets IDs to names
# Via journalctl (systemd systems)
sudo journalctl | grep avc | tail -20
# Watch denials in real time
sudo tail -f /var/log/audit/audit.log | grep avc
# View all denials since last boot
sudo ausearch -m avc -ts boot
A typical AVC denial looks like:
type=AVC msg=audit(1234567890.123:456): avc: denied { read } for
pid=12345 comm="nginx" name="config.php" dev="sda1" ino=67890
scontext=system_u:system_r:httpd_t:s0
tcontext=system_u:object_r:user_home_t:s0
tclass=file permissive=0
Reading this: the nginx process (running as httpd_t) tried to read a file (named config.php) whose type is user_home_t. Policy does not allow httpd_t to read user_home_t files. The denial was enforced (permissive=0).
# Explain what a denial means in plain English
sudo ausearch -m avc -ts recent | audit2why
The most common fix: restorecon
The most frequent SELinux problem on new installations is files with wrong contexts because they were copied from somewhere with a different context. The fix is restorecon:
# Restore correct context for a file or directory
sudo restorecon /var/www/html/index.php
# Restore recursively (after copying a directory into web root)
sudo restorecon -Rv /var/www/html/
# Restore the context for all files in a path (verbose)
sudo restorecon -Rv /etc/nginx/
# Show what restorecon would do without doing it (dry run)
sudo restorecon -Rvn /var/www/html/
# Manually check what the policy says the context should be
sudo matchpathcon /var/www/html/index.php
Changing file contexts
Sometimes you need to set a non-default context because the policy default is wrong for your setup:
# Set a specific context on a file
sudo chcon -t httpd_sys_content_t /srv/myapp/public/index.html
# Set recursively
sudo chcon -Rt httpd_sys_content_t /srv/myapp/public/
# Note: chcon changes are NOT persistent across relabels
# For persistent changes, use semanage fcontext instead:
# Add a policy rule that says /srv/myapp/public(/.*)? should be httpd_sys_content_t
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/myapp/public(/.*)?"
# Apply the new rule
sudo restorecon -Rv /srv/myapp/public/
# List custom fcontext rules you have added
sudo semanage fcontext -l -C
SELinux booleans
Booleans are named toggles for pre-written policy rules. Many common use cases are handled by booleans without needing to write custom policy.
# List all booleans and their current state
getsebool -a
sudo semanage boolean -l # with descriptions
# Search for relevant booleans
getsebool -a | grep httpd
getsebool -a | grep ftp
getsebool -a | grep ssh
# Get the current value of a boolean
getsebool httpd_can_network_connect
# Set a boolean temporarily (resets on reboot)
sudo setsebool httpd_can_network_connect on
# Set a boolean persistently
sudo setsebool -P httpd_can_network_connect on
# Common booleans:
# httpd_can_network_connect -- allow web server outbound connections (reverse proxy)
# httpd_can_connect_db -- allow web server to connect to databases
# httpd_enable_cgi -- allow web server to run CGI scripts
# httpd_use_nfs -- allow web server to read NFS mounts
# ftpd_full_access -- allow FTP daemon full write access
# ssh_chroot_rw_homedirs -- allow chrooted SSH users to write home dirs
# virt_use_usb -- allow VMs to use USB devices
Managing ports
SELinux tracks which ports services are allowed to bind to. If you run a service on a non-standard port, you may need to add it to the policy:
# List SELinux port assignments
sudo semanage port -l
# Find which type covers a port
sudo semanage port -l | grep 8080
sudo semanage port -l | grep ssh
# Add a new port to an existing type
# (e.g., run SSH on port 2222)
sudo semanage port -a -t ssh_port_t -p tcp 2222
# Run nginx on port 8080
sudo semanage port -a -t http_port_t -p tcp 8080
# List ports you have customised
sudo semanage port -l -C
Creating custom policy with audit2allow
When a boolean does not cover your use case, audit2allow turns AVC denials into a custom policy module:
# Generate a policy from all recent denials
sudo ausearch -m avc -ts recent | audit2allow -M mypolicy
# This creates:
# mypolicy.te -- human-readable policy source
# mypolicy.pp -- compiled policy package
# Review what the policy allows (important: understand before loading)
cat mypolicy.te
# Install the policy module
sudo semodule -i mypolicy.pp
# Verify it is loaded
sudo semodule -l | grep mypolicy
# Remove the policy module later if needed
sudo semodule -r mypolicy
Always review the .te file before installing. audit2allow generates exactly what is needed to permit the denied operations, but if your denials were caused by an attack or misconfiguration, you might inadvertently allow something you should not.
Troubleshooting workflow
# 1. Something is broken -- suspect SELinux
sudo setenforce 0 # switch to permissive
# If the problem goes away, SELinux was blocking something
# 2. Find what is being blocked
sudo ausearch -m avc -ts recent -i
sudo ausearch -m avc -ts recent | audit2why
# 3. Common fixes in order of preference:
# a. restorecon (wrong file label)
sudo restorecon -Rv /path/to/files
# b. Enable a boolean (pre-written permission toggle)
sudo setsebool -P httpd_can_network_connect on
# c. Add a port label
sudo semanage port -a -t http_port_t -p tcp 8080
# d. Create custom policy (last resort)
sudo ausearch -m avc -ts recent | audit2allow -M mypolicy
cat mypolicy.te # review before installing
sudo semodule -i mypolicy.pp
# 4. Re-enable enforcing
sudo setenforce 1
Useful SELinux utilities
# Install the SELinux utilities
sudo dnf install policycoreutils-python-utils setools-console
# Show detailed info about a type
sesearch --allow -s httpd_t -t httpd_sys_content_t -c file
# Search what httpd_t is allowed to access
sesearch --allow -s httpd_t
# Check that your policy syntax is correct before compiling
checkmodule -M -m -o mypolicy.mod mypolicy.te
# Relabel the entire filesystem (needed after re-enabling from disabled mode)
sudo touch /.autorelabel
sudo reboot
Frequently Asked Questions
What is SELinux and what problem does it solve?
SELinux (Security-Enhanced Linux) is a mandatory access control (MAC) system implemented as a Linux kernel module. Standard Linux permissions are discretionary (DAC): the file owner decides who can access their files. SELinux adds a second layer: even if standard permissions allow access, SELinux policy can still deny it. This limits the damage a compromised process can do — a hacked web server that could normally read any file the www-data user can access is restricted by SELinux to only the specific files and sockets the policy says a web server process needs. SELinux was originally developed by the NSA and is enabled by default on Fedora, RHEL, CentOS, and their derivatives.
What is the difference between SELinux enforcing and permissive mode?
In enforcing mode, SELinux actively denies operations that violate its policy. A web server trying to connect to a port it is not supposed to use will get a permission denied error, and the denial is logged. In permissive mode, SELinux logs all policy violations but does not deny them — everything still works as if SELinux were off, but the denials show up in the audit log. Permissive mode is used for troubleshooting and for building new policies: you can see what a process needs without breaking it. A third mode, disabled, turns off SELinux entirely and requires a system reboot to re-enable; it is not recommended.
What is an SELinux context?
An SELinux context (also called a label) is metadata attached to every file, process, socket, and other object on the system. It has the form user:role:type:level, for example system_u:object_r:httpd_sys_content_t:s0. The type (httpd_sys_content_t in this example) is the most important part for day-to-day administration: SELinux policy defines which process types can access which object types. Files in /var/www/html have type httpd_sys_content_t; nginx and Apache run with type httpd_t; policy allows httpd_t processes to read httpd_sys_content_t files. You can see contexts with ls -Z (files) and ps -Z (processes).
How do I fix an SELinux denial without disabling SELinux?
First identify the denial with ausearch -m avc -ts recent or journalctl | grep avc. Then run audit2why on the denial to understand the reason. The most common fixes are: restoring the correct file context with restorecon -Rv /path/to/files (most common issue — wrong label after copying files); enabling an SELinux boolean that unlocks a specific behaviour (setsebool -P httpd_can_network_connect on); or generating a custom policy module with audit2allow -a -M mypolicy followed by semodule -i mypolicy.pp. Never set files to a permissive label or disable SELinux as a first response to a denial.
What are SELinux booleans?
SELinux booleans are named on/off switches that toggle pre-written policy rules. Instead of rewriting policy, many common use cases are covered by booleans. For example, httpd_can_network_connect controls whether web servers can make outbound network connections (off by default for security, needed for reverse proxies). getsebool -a lists all booleans and their current state. setsebool httpd_can_network_connect on enables it temporarily (resets on reboot); setsebool -P httpd_can_network_connect on makes it persistent. semanage boolean -l shows booleans with their descriptions.
Why do files copied to /var/www/html cause SELinux denials?
When you copy files, the copy inherits the SELinux context of the source directory, not the destination directory. Files you copy from /home/colton/ into /var/www/html/ will have the user_home_t type from your home directory, not the httpd_sys_content_t type that Apache or nginx is allowed to read. The web server then gets permission denied. The fix is to run restorecon -Rv /var/www/html/ after copying, which resets all file contexts to the policy-default for that path. Alternatively, use cp —preserve=context to preserve the target context during copy, or use rsync with —no-perms.