
If you’ve spent time on Fedora or RHEL systems, you’ve encountered SELinux. And if you’re like many administrators, your first instinct when something doesn’t work is to check if SELinux is the culprit. The temptation to run setenforce 0 is strong - but it’s also the wrong approach.
SELinux (Security-Enhanced Linux) implements mandatory access control (MAC) at the kernel level. Unlike traditional Unix permissions (discretionary access control), SELinux policies are enforced regardless of file ownership or traditional permissions. A process running as root can still be blocked from accessing files if the SELinux policy doesn’t permit it.
This guide focuses on practical skills: understanding what SELinux is doing, troubleshooting denials, and configuring policies correctly. We’ll cover the concepts you need without getting lost in policy theory.
Most of the tools in this guide (semanage, audit2allow, restorecon) come from the policycoreutils-python-utils package, which isn’t always installed on minimal systems:
sudo dnf install policycoreutils-python-utils setroubleshoot-server
Why SELinux Matters
SELinux isn’t security theater. It provides genuine defense-in-depth:
Confinement: Even compromised root processes are limited by SELinux policy. A web server exploit that gains root can’t suddenly read SSH keys or modify system binaries.
Least privilege: Policies define exactly what each process type can access. Apache doesn’t need to read /etc/shadow, so it can’t.
Audit trail: Every denial is logged. Attempted intrusions leave fingerprints even when blocked.
The alternative - running in permissive mode or disabling SELinux entirely - removes this protection layer. The goal is to work with SELinux, not around it.
SELinux Modes
SELinux operates in three modes:
| Mode | Behavior | Use Case |
|---|---|---|
| Enforcing | Policy is enforced, denials logged and blocked | Production systems |
| Permissive | Policy checked, denials logged but not blocked | Debugging, policy development |
| Disabled | SELinux entirely inactive | Not recommended |
Check current status:
sestatus
Output shows the mode, policy name, and other details:
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
Temporarily switch modes:
# Switch to permissive (survives until reboot)
sudo setenforce 0
# Return to enforcing
sudo setenforce 1
For permanent changes, edit /etc/selinux/config:
SELINUX=enforcing
SELINUXTYPE=targeted
The targeted policy is standard on Fedora and RHEL - it confines specific daemons while leaving user processes largely unrestricted. Other policy types exist (mls for multi-level security) but are niche.
Understanding SELinux Contexts
Everything in SELinux revolves around contexts (also called labels). Every process, file, socket, and network port has a security context that determines what can access what.
Viewing Contexts
Files and directories:
ls -Z /var/www/html/index.html
unconfined_u:object_r:httpd_sys_content_t:s0 /var/www/html/index.html
Processes:
ps -Z -C httpd
LABEL PID TTY TIME CMD
system_u:system_r:httpd_t:s0 1234 ? 00:00:01 httpd
Ports:
semanage port -l | grep http
http_cache_port_t tcp 8080, 8118, 8123, 10001-10010
http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000
Context Format
Contexts have four colon-separated fields:
user:role:type:level
For practical administration, the type field (httpd_sys_content_t, httpd_t) matters most. Types ending in _t are what policies match against.
- User (
system_u,unconfined_u): SELinux user, maps to Linux users - Role (
system_r,object_r): Groups types for role-based access - Type (
httpd_t,httpd_sys_content_t): The primary policy discriminator - Level (
s0): MLS/MCS sensitivity level (usually s0 in targeted policy)
How Policy Works
The policy defines rules like:
“A process with type
httpd_tcan read files with typehttpd_sys_content_t“
This is why placing a file in /var/www/html/ doesn’t automatically make it readable by Apache - the context must match. A file with user_home_t context won’t be accessible even if owned by apache:apache.
Common Administrative Tasks
Changing File Contexts
When you move or create files outside standard locations, they won’t have the correct context. This often catches people off guard: cp inherits the destination directory’s context, but mv preserves the original context. Moving a file from your home directory into /var/www/html/ leaves it with user_home_t - and Apache can’t read it despite the file being in the right place.
Two approaches exist to fix contexts:
Temporary (relabel gets reset):
chcon -R -t httpd_sys_content_t /srv/www/
Persistent (survives relabels):
# Define the context mapping
semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"
# Apply the context
restorecon -Rv /srv/www/
The semanage fcontext command adds rules to the policy. restorecon applies those rules. This is the correct approach for production.
List custom context mappings:
semanage fcontext -l | grep srv
Managing Port Labels
Running a service on a non-standard port? The port needs the correct label:
# Allow Apache to listen on port 8081
semanage port -a -t http_port_t -p tcp 8081
Remove a custom port mapping:
semanage port -d -t http_port_t -p tcp 8081
SELinux Booleans
Booleans toggle specific policy behaviors without rewriting policy. They’re the preferred way to enable common functionality:
# List booleans related to HTTP
semanage boolean -l | grep httpd
# Or using getsebool
getsebool -a | grep httpd
Common HTTP booleans:
# Allow Apache to connect to network (for reverse proxy)
setsebool -P httpd_can_network_connect 1
# Allow Apache to send email
setsebool -P httpd_can_sendmail 1
# Allow home directory access
setsebool -P httpd_enable_homedirs 1
# Allow network relay (for mod_proxy)
setsebool -P httpd_can_network_relay 1
The -P flag makes changes persistent. Without it, changes reset on reboot.
Troubleshooting SELinux Denials
When something doesn’t work and you suspect SELinux, here’s the systematic approach:
Step 1: Confirm It’s SELinux
Temporarily switch to permissive:
sudo setenforce 0
If the problem goes away, SELinux is the cause. Switch back to enforcing while you debug:
sudo setenforce 1
Step 2: Find the Denial
SELinux denials are logged to /var/log/audit/audit.log. The format is dense, but tools help:
# Check recent denials
ausearch -m AVC -ts recent
# More readable format
ausearch -m AVC -ts recent | audit2allow -w
Example output:
Type: AVC
Summary:
SELinux is preventing /usr/sbin/httpd from write access on the directory /var/www/uploads.
***** Plugin catchall_boolean (89.3 confidence) suggests ******************
If you want to allow httpd to have unified access to all httpd content
Then you must tell SELinux about this by enabling the 'httpd_unified' boolean.
Do
setsebool -P httpd_unified 1
For systems without auditd, check /var/log/messages for setroubleshoot messages.
Step 3: Understand the Denial
Raw audit entry:
type=AVC msg=audit(1708032000.123:456): avc: denied { write } for pid=1234 comm="httpd" name="uploads" dev="dm-0" ino=789 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:httpd_sys_content_t:s0 tclass=dir permissive=0
Key fields:
scontext: Source context (the process)tcontext: Target context (what was accessed)tclass: Object class (file, dir, socket){ write }: The denied permission
Step 4: Fix the Denial
Options, from best to worst:
1. Boolean (preferred for common cases):
audit2allow -w -a
Often suggests a boolean. Enable it.
2. Context fix (for mislabeled files):
# Check current context
ls -Z /var/www/uploads/
# Fix to appropriate type
semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/uploads(/.*)?"
restorecon -Rv /var/www/uploads/
3. Custom policy module (for application-specific needs):
# Generate policy from audit log
ausearch -c 'myapp' --raw | audit2allow -M myapp
# Apply the module
semodule -i myapp.pp
4. Per-domain permissive (temporary workaround):
# Make httpd permissive while keeping system enforcing
semanage permissive -a httpd_t
Never disable SELinux globally for an application-specific issue.
SELinux and Containers
Podman and containers have specific SELinux considerations. The container runtime automatically sets up isolation, but volume mounts require attention.
Container File Labels
When mounting host directories into containers, SELinux contexts matter. Podman provides flags to handle this:
# The :z flag relabels the directory for container use
podman run -v /srv/data:/data:z nginx
# The :Z flag makes the volume private to this container
podman run -v /srv/data:/data:Z nginx
- :z (lowercase): Relabels with
container_file_t, shared among containers - :Z (uppercase): Private labeling, only this container can access
Warning: Both flags relabel the host directory. If another service on the host depends on the original label, :z or :Z will break it. Only use these on directories dedicated to container use.
Quadlet Example
In Podman Quadlet files:
[Container]
Image=docker.io/library/nginx:latest
Volume=/srv/web:/usr/share/nginx/html:z
The :z flag ensures SELinux doesn’t block the container from reading mounted files.
Checking Container Contexts
# Container processes run in container_t
ps -Z | grep container
# Files in container volumes should have container_file_t
ls -Z /srv/web
Relabeling the Filesystem
If contexts get completely mangled (or after restoring from backup), force a full relabel:
# Create relabel trigger
touch /.autorelabel
# Reboot
reboot
The system relabels all files on next boot. This takes time on large filesystems.
For targeted relabeling:
restorecon -Rv /path/to/directory
Common Scenarios
Web Server with Non-Standard Document Root
# Set context for new web root
semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"
restorecon -Rv /srv/www
# For writable directories (uploads, cache)
semanage fcontext -a -t httpd_sys_rw_content_t "/srv/www/uploads(/.*)?"
restorecon -Rv /srv/www/uploads
Database with Custom Data Directory
# PostgreSQL
semanage fcontext -a -t postgresql_db_t "/srv/pgdata(/.*)?"
restorecon -Rv /srv/pgdata
# MariaDB/MySQL
semanage fcontext -a -t mysqld_db_t "/srv/mysqldata(/.*)?"
restorecon -Rv /srv/mysqldata
SSH on Non-Standard Port
# Add port to ssh_port_t
semanage port -a -t ssh_port_t -p tcp 2222
# Verify
semanage port -l | grep ssh
Home Directory Web Access
# Enable userdir module access
setsebool -P httpd_enable_homedirs 1
# For specific user home access (more restrictive)
setsebool -P httpd_read_user_content 1
Useful Commands Reference
| Task | Command |
|---|---|
| Check status | sestatus |
| List file contexts | ls -Z |
| List process contexts | ps -Z |
| Change file context | chcon -t type file |
| Add persistent context | semanage fcontext -a -t type "/path(/.*)?" |
| Apply contexts | restorecon -Rv /path |
| List booleans | getsebool -a or semanage boolean -l |
| Set boolean | setsebool -P boolean_name 1 |
| List port labels | semanage port -l |
| Add port label | semanage port -a -t type -p tcp port |
| Search denials | ausearch -m AVC -ts recent |
| Explain denials | audit2allow -w -a |
| Create policy module | ausearch --raw \| audit2allow -M name |
| Install module | semodule -i name.pp |
| List modules | semodule -l |
Best Practices
Never disable SELinux globally. If an application has issues, troubleshoot the specific denial.
Use booleans first. Many common use cases have pre-built toggles.
Prefer context changes over policy changes. Labeling files correctly is cleaner than adding policy rules.
Document customizations. Track semanage commands in configuration management (Ansible, Salt) so systems remain reproducible.
Test in permissive first. When developing policy, run in permissive to collect all denials before switching to enforcing.
Use audit2allow -w for explanations. Before creating a policy module, understand what you’re allowing.
Conclusion
SELinux is a powerful security layer that protects systems even when traditional permissions are bypassed. The learning curve is real, but the skills are essential for Fedora and RHEL administration.
The key insight: SELinux isn’t trying to break your applications. When denials occur, it’s usually because files or ports are mislabeled, or a boolean needs to be set. The tools exist to diagnose and fix these issues correctly - use them instead of disabling the protection.
Once you understand contexts, booleans, and the troubleshooting workflow, SELinux becomes a tool you work with rather than an obstacle to work around.
References
SELinux was born from the NSA’s research into mandatory access controls - military-grade security that somehow became a default on every Fedora workstation and RHEL server. Most of the time it’s invisible, doing its job without asking. When it does push back, it’s specific enough to tell you exactly what went wrong and how to fix it. In a world where privilege escalation makes regular headlines, a kernel-level layer that constrains even root isn’t an inconvenience - it’s quiet insurance.
Comments
You can use your Mastodon or other ActivityPub account to comment on this article by replying to the associated post.
Search for the copied link on your Mastodon instance to reply.
Loading comments...