Mastodon

SELinux: A Practical Guide for Fedora and RHEL



SELinux

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_t can read files with type httpd_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...