AlmaLinux

Troubleshoot SELinux on Rocky Linux 10 / AlmaLinux 10 / RHEL 10

SELinux (Security-Enhanced Linux) is a mandatory access control system built into the Linux kernel. It restricts what processes can do – even if they run as root – by enforcing security policies based on labels and rules. On Rocky Linux 10, AlmaLinux 10, and RHEL 10, SELinux ships enabled in enforcing mode with the targeted policy. This guide covers how to troubleshoot SELinux denials using tools like sealert, restorecon, audit2allow, and semanage on RHEL 10-family systems.

Every section includes the exact commands, expected output, and explanations so you can diagnose and fix SELinux issues without disabling it. The examples target Rocky Linux 10, AlmaLinux 10, and RHEL 10 but apply to any system running SELinux with the targeted policy. For full upstream documentation, see the Red Hat SELinux documentation.

SELinux Overview

SELinux enforces Mandatory Access Control (MAC) on top of the standard Linux permissions (DAC). Every process, file, port, and socket gets a security label called a context. The SELinux policy defines which contexts can interact with each other. If a process tries something the policy does not allow, the kernel blocks it and logs an AVC (Access Vector Cache) denial.

SELinux runs in three modes. Enforcing blocks and logs policy violations. Permissive logs violations but does not block them – useful for debugging. Disabled turns SELinux off entirely, which is not recommended for production. The default and recommended mode is enforcing.

The targeted policy is the default on RHEL-family systems. It confines specific services (httpd, named, postgresql, etc.) while leaving most user processes unconfined. Each confined service runs in its own domain type (like httpd_t or postgresql_t), and file access is controlled through type labels like httpd_sys_content_t or postgresql_db_t.

SELinux booleans are on/off switches that adjust the policy without writing custom rules. For example, the httpd_can_network_connect boolean controls whether the web server can make outbound network connections. Booleans let you tune the policy for your environment without weakening the overall security model.

Prerequisites

  • Rocky Linux 10, AlmaLinux 10, or RHEL 10 server
  • Root or sudo access
  • SELinux enabled (default on all RHEL-family systems)
  • Basic familiarity with Linux command line and systemd services

Step 1: Check SELinux Status and Mode

Start by confirming SELinux is active and which mode it runs in. The quickest check is getenforce.

getenforce

This returns a single word showing the current mode – Enforcing, Permissive, or Disabled:

Enforcing

For more detail including the policy type and loaded policy version, use sestatus.

sestatus

The output shows the SELinux status, current mode, the mode from the config file, and the active policy:

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

The persistent SELinux configuration lives in /etc/selinux/config. Open it to verify or change the boot-time mode.

sudo vi /etc/selinux/config

The key setting is the SELINUX directive:

# SELINUX= can take one of these three values:
#     enforcing - SELinux security policy is enforced.
#     permissive - SELinux prints warnings instead of enforcing.
#     disabled - No SELinux policy is loaded.
SELINUX=enforcing

# SELINUXTYPE= can take one of these values:
#     targeted - Targeted processes are protected.
#     minimum - Modification of targeted policy. Only selected processes are protected.
#     mls - Multi Level Security protection.
SELINUXTYPE=targeted

To temporarily switch between enforcing and permissive (no reboot required), use setenforce. This is useful when troubleshooting – switch to permissive, reproduce the issue, then check the logs.

sudo setenforce 0

This sets SELinux to permissive mode immediately. Confirm the change:

getenforce

You should see the mode has changed:

Permissive

Switch back to enforcing when done troubleshooting:

sudo setenforce 1

The setenforce change does not survive a reboot. To make mode changes permanent, edit /etc/selinux/config and set the SELINUX= line. If you need to fully disable SELinux, changing the config file and rebooting is the only way.

Step 2: Understanding SELinux Contexts

Every file, process, and user on an SELinux system carries a security context in the format user:role:type:level. The most important field for troubleshooting is the type – this is what the targeted policy uses for access decisions.

View file contexts with ls -Z:

ls -Z /var/www/html/

The output shows the SELinux context for each file. The type field (third element) determines which processes can access the file:

unconfined_u:object_r:httpd_sys_content_t:s0 index.html

View process contexts with ps -Z:

ps -eZ | grep httpd

This shows the domain type each process runs in. The httpd workers run in the httpd_t domain:

system_u:system_r:httpd_t:s0     1234 ?        00:00:00 httpd
system_u:system_r:httpd_t:s0     1235 ?        00:00:00 httpd

Check your own user context with id -Z:

id -Z

A typical output for a root login:

unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

Here is what each field means:

  • user (system_u, unconfined_u) – the SELinux user identity, mapped from the Linux user
  • role (system_r, object_r) – determines which types a user can enter. Processes use system_r, files use object_r
  • type (httpd_t, httpd_sys_content_t) – the main field for access control decisions. A process in httpd_t can read files labeled httpd_sys_content_t
  • level (s0) – MLS/MCS security level, typically s0 on targeted policy systems

Common file types you will encounter:

Type LabelUsed For
httpd_sys_content_tWeb server static content (read-only by httpd)
httpd_sys_rw_content_tWeb content that httpd can read and write
postgresql_db_tPostgreSQL database files
mysqld_db_tMariaDB/MySQL database files
var_log_tSystem log files
admin_home_tAdmin user home directory files
user_home_tRegular user home directory files
container_file_tContainer-accessible files (Podman/Docker)

Step 3: Install SELinux Troubleshooting Tools

The base system includes SELinux but not the troubleshooting tools. Install these three packages to get the full toolkit.

sudo dnf install -y setroubleshoot-server setools-console policycoreutils-python-utils

Here is what each package provides:

  • setroubleshoot-server – the sealert tool that translates raw AVC denials into human-readable diagnostics with suggested fixes
  • setools-console – analysis tools like sesearch, seinfo, and sefcontext for querying the loaded policy
  • policycoreutils-python-utils – the semanage, audit2allow, and audit2why commands for managing contexts, ports, booleans, and building custom policy modules

After installing setroubleshoot-server, restart the auditd service so it picks up the new setroubleshoot plugin:

sudo service auditd restart

Step 4: Reading SELinux Denials

When SELinux blocks something, it writes an AVC denial message to the audit log at /var/log/audit/audit.log. The ausearch command filters these messages efficiently.

sudo ausearch -m AVC -ts recent

The -ts recent flag limits results to the last 10 minutes. A typical AVC denial looks like this:

type=AVC msg=audit(1737534445.414:326): avc:  denied  { getattr } for  pid=12517 comm="httpd" path="/var/www/html/sample_website" dev="dm-0" ino=67241070 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:admin_home_t:s0 tclass=file permissive=0

Breaking down the key fields:

  • denied { getattr } – the permission that was blocked (read, write, getattr, open, etc.)
  • comm=”httpd” – the process name that triggered the denial
  • path=”/var/www/html/sample_website” – the target file or resource
  • scontext=system_u:system_r:httpd_t:s0 – the source (process) context. The type httpd_t is the key
  • tcontext=unconfined_u:object_r:admin_home_t:s0 – the target (file) context. The type admin_home_t is wrong for web content
  • tclass=file – the object class (file, dir, socket, port, etc.)
  • permissive=0 – 0 means enforcing (the action was blocked), 1 means permissive (logged but allowed)

You can also search for denials with journalctl:

sudo journalctl -t setroubleshoot --since "10 minutes ago"

Or search the audit log directly for AVC messages from a specific process:

sudo ausearch -m AVC -c httpd

Step 5: Using sealert for Human-Readable Diagnostics

Raw AVC messages are dense. The sealert tool from the setroubleshoot package analyzes them and produces plain-language explanations with suggested fixes ranked by confidence.

sudo sealert -a /var/log/audit/audit.log

The output groups related denials and provides ranked solutions. Here is an example for a web server file access denial:

SELinux is preventing /usr/sbin/httpd from getattr access on the file /var/www/html/sample_website.

*****  Plugin restorecon (99.5 confidence) suggests   ************************

If you want to fix the label.
/var/www/html/sample_website default label should be httpd_sys_content_t.
Then you can run restorecon. The access attempt may have been stopped due to
insufficient permissions to access a parent directory in which case try to
change the following command accordingly.
Do
# /sbin/restorecon -v /var/www/html/sample_website

*****  Plugin catchall (1.49 confidence) suggests   **************************

If you believe that httpd should be allowed getattr access on the
sample_website file by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# ausearch -c 'httpd' --raw | audit2allow -M my-httpd
# semodule -X 300 -i my-httpd.pp

The key things to notice in sealert output:

  • The confidence score – higher means more likely correct. Always try the highest-confidence suggestion first
  • The restorecon plugin at 99.5% confidence means the file has a wrong label – this is the most common issue
  • The catchall plugin at 1.49% suggests building a custom policy module – this is a last resort, not a first step

When setroubleshoot is running, it also logs summaries to /var/log/messages with a sealert command ID you can look up directly:

sudo sealert -l fe80ac67-a100-4244-a782-c56343b41ad7

Step 6: Fix Context Issues with restorecon and semanage

Wrong file contexts are the number one cause of SELinux denials. Files get the wrong label when they are moved (not copied) from another location, created in a non-standard directory, or when the filesystem was relabeled incorrectly.

The restorecon command resets file labels to the system default for that path. Use it after moving files into a web root or other service directory:

sudo restorecon -Rv /var/www/html/

The -R flag is recursive and -v shows each file relabeled. The output confirms what changed:

Relabeled /var/www/html/sample_website from unconfined_u:object_r:admin_home_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0

When you use a custom directory outside the standard paths, you need to define a permanent context rule with semanage fcontext and then apply it with restorecon.

Custom web root example – set /data/www as web content:

sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"

Apply the new context rule to existing files:

sudo restorecon -Rv /data/www/

Writable web directory example – set a directory where the web server needs to write (uploads, cache):

sudo semanage fcontext -a -t httpd_sys_rw_content_t "/data/www/uploads(/.*)?"

Apply the context:

sudo restorecon -Rv /data/www/uploads/

To verify the context was set correctly:

ls -Zd /data/www/ /data/www/uploads/

You should see the correct type labels on both directories:

unconfined_u:object_r:httpd_sys_content_t:s0 /data/www/
unconfined_u:object_r:httpd_sys_rw_content_t:s0 /data/www/uploads/

To list all custom file context rules you have added:

sudo semanage fcontext -l -C

To remove a custom context rule:

sudo semanage fcontext -d "/data/www(/.*)?"

Step 7: Managing SELinux Booleans

Booleans are policy switches that enable or disable specific capabilities for confined services. They let you grant access without writing custom policy modules.

List all booleans and their current state:

getsebool -a

Filter for a specific service – for example, all httpd-related booleans:

getsebool -a | grep httpd

The output lists each boolean and its current state (on or off):

httpd_anon_write --> off
httpd_builtin_scripting --> on
httpd_can_check_spam --> off
httpd_can_connect_ftp --> off
httpd_can_connect_ldap --> off
httpd_can_network_connect --> off
httpd_can_network_connect_db --> off
httpd_can_network_relay --> off
httpd_can_sendmail --> off
httpd_enable_cgi --> on
httpd_enable_homedirs --> off
httpd_read_user_content --> off
httpd_use_nfs --> off

Enable a boolean persistently with the -P flag (without -P the change is lost on reboot):

sudo setsebool -P httpd_can_network_connect on

Here are the most commonly needed booleans for web and database services:

BooleanWhat It Allows
httpd_can_network_connecthttpd can make outbound TCP connections (proxy to app servers, APIs)
httpd_can_network_connect_dbhttpd can connect to database ports (MySQL, PostgreSQL)
httpd_can_sendmailhttpd/PHP can send email via sendmail/postfix
httpd_enable_homedirshttpd can serve content from user home directories
httpd_use_nfshttpd can access NFS-mounted content
httpd_use_cifshttpd can access CIFS/Samba-mounted content
httpd_execmemhttpd can execute memory-mapped code (some PHP extensions need this)
mysql_connect_anyMySQL/MariaDB can connect to any port
nis_enabledAllow services to use NIS/NIS+ networking

Use semanage boolean -l to see a description of each boolean along with its default and current values.

Step 8: Creating Custom Policies with audit2allow

When restorecon and booleans do not resolve a denial, audit2allow can generate a custom policy module from the audit log entries. This should be a last resort – always try context fixes and booleans first. If you manage SELinux across multiple servers, consider using Ansible to manage SELinux policies at scale.

First, view what rule would be needed to allow the denied action:

sudo ausearch -m AVC -ts recent | audit2allow

This shows the Type Enforcement (TE) rule that would allow the denied access:

#============= httpd_t ==============
allow httpd_t admin_home_t:file getattr;

Review the rule carefully. If it looks reasonable, generate a compiled policy module:

sudo ausearch -m AVC -ts recent | audit2allow -M my-httpd-fix

This creates two files – a .te (type enforcement source) and a .pp (compiled policy package):

******************** IMPORTANT ***********************
To make this policy package active, execute:

semodule -i my-httpd-fix.pp

Install the policy module:

sudo semodule -i my-httpd-fix.pp

Verify the module was loaded:

sudo semodule -l | grep my-httpd-fix

Warnings about audit2allow:

  • Never run audit2allow against the entire audit log blindly. Filter for the specific process first: ausearch -c httpd
  • Do not generate modules while in permissive mode without reviewing – permissive mode collects all potential denials, many of which should not be allowed
  • Always review the .te file before installing. A rule like allow httpd_t self:capability dac_override might indicate a real file permission problem, not a policy gap
  • Use audit2why first to understand why the denial happened – it often points to a boolean you can enable instead

To remove a custom module later:

sudo semodule -r my-httpd-fix

Step 9: Managing SELinux Ports

SELinux labels network ports the same way it labels files. If a service tries to listen on a port not assigned to its type, SELinux blocks it. The semanage port command manages port labels.

List all port labels for a specific type:

sudo semanage port -l | grep http_port_t

The output shows which ports are labeled for HTTP use:

http_port_t                    tcp      80, 81, 443, 488, 8008, 8009, 8443, 9000

To allow a web server to listen on a custom port like 8090:

sudo semanage port -a -t http_port_t -p tcp 8090

Custom SSH port – if you change SSH to port 2222:

sudo semanage port -a -t ssh_port_t -p tcp 2222

Custom database port – if PostgreSQL runs on port 5433 (for managing custom PostgreSQL ports with SELinux):

sudo semanage port -a -t postgresql_port_t -p tcp 5433

Verify the port was added:

sudo semanage port -l | grep 5433

You should see the port listed under the correct type:

postgresql_port_t              tcp      5433, 5432

To list only custom (locally modified) port definitions:

sudo semanage port -l -C

To remove a custom port label:

sudo semanage port -d -t http_port_t -p tcp 8090

Step 10: Real-World SELinux Troubleshooting Examples

Nginx Serving from /data/www

You configure Nginx on Rocky Linux with a custom document root at /data/www. After restarting Nginx, you get 403 Forbidden. Check the audit log:

sudo ausearch -m AVC -c nginx -ts recent

The denial shows Nginx in httpd_t trying to read files with default_t (the generic label for files with no specific rule). Fix it by setting the correct context:

sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www/

If Nginx also needs to write to a cache or upload directory under /data/www/cache:

sudo semanage fcontext -a -t httpd_sys_rw_content_t "/data/www/cache(/.*)?"
sudo restorecon -Rv /data/www/cache/

PostgreSQL on Non-Standard Port

You change PostgreSQL to listen on port 5433 in /var/lib/pgsql/data/postgresql.conf. PostgreSQL fails to start. Check the denial:

sudo ausearch -m AVC -c postgres

The AVC message shows postgresql_t denied binding to port 5433. Add the port label:

sudo semanage port -a -t postgresql_port_t -p tcp 5433
sudo systemctl restart postgresql

Postfix Cannot Send Mail from Web Application

Your PHP application sends email through Postfix but messages never leave the server. The AVC log shows httpd_t denied connecting to postfix_master_t. The fix is a boolean:

sudo setsebool -P httpd_can_sendmail on

If the application also needs to make outbound SMTP connections to an external relay:

sudo setsebool -P httpd_can_network_connect on

Podman Container Access Denied to Host Volume

You mount a host directory into a Podman container with -v /opt/appdata:/data and the container gets permission denied errors. SELinux blocks the container process from reading host files. You can set the correct SELinux context for Podman volumes using the :Z or :z suffix:

podman run -v /opt/appdata:/data:Z myapp

The :Z suffix tells Podman to relabel the host directory with container_file_t and set a private label (only this container can access it). Use :z for shared access across multiple containers.

If you prefer to set the context manually:

sudo semanage fcontext -a -t container_file_t "/opt/appdata(/.*)?"
sudo restorecon -Rv /opt/appdata/

SELinux Command Reference

CommandPurpose
getenforceShow current SELinux mode (Enforcing/Permissive/Disabled)
sestatusShow detailed SELinux status and policy info
setenforce 0|1Temporarily switch to Permissive (0) or Enforcing (1)
ls -ZShow file SELinux contexts
ps -eZShow process SELinux contexts
id -ZShow current user SELinux context
ausearch -m AVCSearch audit log for SELinux denials
sealert -a /var/log/audit/audit.logAnalyze all denials with human-readable output
restorecon -Rv /pathReset file contexts to system defaults
semanage fcontext -a -t TYPE "path(/.*)?"Add permanent file context rule
semanage fcontext -l -CList custom (locally added) file context rules
getsebool -aList all SELinux booleans and their state
setsebool -P boolean on|offSet a boolean persistently
semanage port -a -t TYPE -p tcp PORTAdd a port label
semanage port -lList all port labels
audit2allow -aShow what TE rules would allow current denials
audit2allow -a -M moduleGenerate a compiled policy module from denials
semodule -i module.ppInstall a custom policy module
semodule -r moduleRemove a custom policy module

Conclusion

You now have a working toolkit for diagnosing and fixing SELinux issues on Rocky Linux 10, AlmaLinux 10, and RHEL 10. The troubleshooting workflow follows a consistent pattern: check the AVC denial with ausearch, analyze it with sealert, then fix it using restorecon (wrong label), setsebool (missing boolean), semanage port (wrong port label), or audit2allow (custom policy) depending on the root cause. The full upstream documentation is available at the SELinux project on GitHub.

In production, keep SELinux in enforcing mode and resist the temptation to disable it. Use permissive mode only during active troubleshooting, and always switch back to enforcing once the issue is resolved. Custom audit2allow modules should be reviewed carefully and kept minimal – most real-world issues are solved by restorecon, booleans, or port labels.

Related Articles

AlmaLinux Rocky/Alma 8: Deploy HA Redis Cluster (Sentinel) Security OpenVPN Server Configuration on RHEL / Rocky / Alma Security How To Install OpenSSL on Windows Server 2019 VPN Install Cisco AnyConnect VPN on Ubuntu, Debian, and Fedora

Press ESC to close