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 usesystem_r, files useobject_r - type (
httpd_t,httpd_sys_content_t) – the main field for access control decisions. A process inhttpd_tcan read files labeledhttpd_sys_content_t - level (
s0) – MLS/MCS security level, typicallys0on targeted policy systems
Common file types you will encounter:
| Type Label | Used For |
|---|---|
httpd_sys_content_t | Web server static content (read-only by httpd) |
httpd_sys_rw_content_t | Web content that httpd can read and write |
postgresql_db_t | PostgreSQL database files |
mysqld_db_t | MariaDB/MySQL database files |
var_log_t | System log files |
admin_home_t | Admin user home directory files |
user_home_t | Regular user home directory files |
container_file_t | Container-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
sealerttool that translates raw AVC denials into human-readable diagnostics with suggested fixes - setools-console – analysis tools like
sesearch,seinfo, andsefcontextfor querying the loaded policy - policycoreutils-python-utils – the
semanage,audit2allow, andaudit2whycommands 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_tis the key - tcontext=unconfined_u:object_r:admin_home_t:s0 – the target (file) context. The type
admin_home_tis 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:
| Boolean | What It Allows |
|---|---|
httpd_can_network_connect | httpd can make outbound TCP connections (proxy to app servers, APIs) |
httpd_can_network_connect_db | httpd can connect to database ports (MySQL, PostgreSQL) |
httpd_can_sendmail | httpd/PHP can send email via sendmail/postfix |
httpd_enable_homedirs | httpd can serve content from user home directories |
httpd_use_nfs | httpd can access NFS-mounted content |
httpd_use_cifs | httpd can access CIFS/Samba-mounted content |
httpd_execmem | httpd can execute memory-mapped code (some PHP extensions need this) |
mysql_connect_any | MySQL/MariaDB can connect to any port |
nis_enabled | Allow 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
audit2allowagainst 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
.tefile before installing. A rule likeallow httpd_t self:capability dac_overridemight indicate a real file permission problem, not a policy gap - Use
audit2whyfirst 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
| Command | Purpose |
|---|---|
getenforce | Show current SELinux mode (Enforcing/Permissive/Disabled) |
sestatus | Show detailed SELinux status and policy info |
setenforce 0|1 | Temporarily switch to Permissive (0) or Enforcing (1) |
ls -Z | Show file SELinux contexts |
ps -eZ | Show process SELinux contexts |
id -Z | Show current user SELinux context |
ausearch -m AVC | Search audit log for SELinux denials |
sealert -a /var/log/audit/audit.log | Analyze all denials with human-readable output |
restorecon -Rv /path | Reset file contexts to system defaults |
semanage fcontext -a -t TYPE "path(/.*)?" | Add permanent file context rule |
semanage fcontext -l -C | List custom (locally added) file context rules |
getsebool -a | List all SELinux booleans and their state |
setsebool -P boolean on|off | Set a boolean persistently |
semanage port -a -t TYPE -p tcp PORT | Add a port label |
semanage port -l | List all port labels |
audit2allow -a | Show what TE rules would allow current denials |
audit2allow -a -M module | Generate a compiled policy module from denials |
semodule -i module.pp | Install a custom policy module |
semodule -r module | Remove 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.