How to Configure Mandatory Access Control with SELinux Policies on RHEL 7
SELinux (Security-Enhanced Linux) is a mandatory access control (MAC) framework built into the Linux kernel and deeply integrated into RHEL 7. Unlike discretionary access control (DAC), which is based on user and group ownership, SELinux enforces fine-grained access rules based on security contexts — labels attached to every process, file, port, and device on the system. The targeted policy, which ships with RHEL 7, confines high-risk system daemons while leaving most user processes unconfined. When your applications violate the default policy — for example, a web server writing to a non-standard directory or listening on an unusual port — you must write or extend SELinux policy modules rather than disabling SELinux entirely. This guide covers the complete workflow for creating, debugging, and deploying custom SELinux policy modules on RHEL 7.
Prerequisites
- RHEL 7 with SELinux in enforcing or permissive mode (check with
getenforce) - Root or sudo access
- The
policycoreutils-python,policycoreutils-devel, andsetools-consolepackages - Basic familiarity with SELinux contexts and the targeted policy
- An application that is generating AVC denial messages in the audit log
# Install required SELinux policy development tools
yum install -y policycoreutils-python policycoreutils-devel setools-console
checkpolicy setroubleshoot-server
Step 1: Understanding Type Enforcement and Security Contexts
Every object in an SELinux system has a security context in the form user:role:type:level. The type component (also called a domain for processes) is the most important for the targeted policy. Type enforcement (TE) rules describe which source type (domain) can perform which operations on which target type. View the context of a running process and a file:
# View process contexts
ps -eZ | grep httpd
# Example output:
# system_u:system_r:httpd_t:s0 12345 ? 00:00:01 httpd
# View file contexts
ls -Z /var/www/html/
# Example output:
# -rw-r--r--. root root unconfined_u:object_r:httpd_sys_content_t:s0 index.html
# View context of a port
semanage port -l | grep http
Type enforcement (.te) source files define rules in the format:
# Allow domain myapp_t to read files of type myapp_data_t
allow myapp_t myapp_data_t:file { read getattr open };
# Allow myapp_t to connect to a tcp socket
allow myapp_t http_port_t:tcp_socket name_connect;
Step 2: Identifying AVC Denials
When SELinux blocks an operation, it logs an AVC (Access Vector Cache) denial to /var/log/audit/audit.log. These denials are the starting point for writing a custom policy. Set SELinux to permissive mode temporarily to allow the application to run completely and generate all necessary denials before writing policy:
# Set a specific domain to permissive (better than global permissive)
semanage permissive -a myapp_t
# Or set global permissive mode temporarily
setenforce 0
# Run your application and trigger all the code paths
# Search for AVC denials in the audit log
grep "type=AVC" /var/log/audit/audit.log | tail -20
# Use audit2why for human-readable explanations
grep "type=AVC" /var/log/audit/audit.log | audit2why
A typical AVC denial in the audit log looks like:
type=AVC msg=audit(1747483201.123:456): avc: denied { write } for pid=1234 comm="myapp" name="data.log" dev="sda1" ino=789012 scontext=system_u:system_r:myapp_t:s0 tcontext=system_u:object_r:var_log_t:s0 tclass=file permissive=0
The audit2why tool explains why the denial occurred and often suggests a boolean or policy change to resolve it.
Step 3: The audit2allow Workflow
The audit2allow tool reads AVC denial messages and generates policy rules that would permit the denied operations. This is the primary tool for generating custom policy modules:
# Generate a policy module from all recent AVC denials for a specific process
grep "type=AVC" /var/log/audit/audit.log | grep "myapp" | audit2allow -M myapp_policy
# This creates two files:
# myapp_policy.te — the Type Enforcement source file
# myapp_policy.pp — the compiled policy package
# Review the generated .te file before compiling
cat myapp_policy.te
A generated .te file looks like:
module myapp_policy 1.0;
require {
type myapp_t;
type var_log_t;
type http_port_t;
class file { write create open append getattr };
class tcp_socket name_connect;
}
#============= myapp_t ==============
allow myapp_t var_log_t:file { write create open append getattr };
allow myapp_t http_port_t:tcp_socket name_connect;
Always review the generated rules. Overly broad rules generated by audit2allow (such as allow myapp_t unlabeled_t:file read) may indicate a labeling problem rather than a policy gap.
Step 4: Compiling and Installing a Custom Policy Module
The policy compilation process converts the human-readable .te source into a binary .pp (policy package) that can be loaded into the kernel:
# Step 1: Check and compile the .te file into a .mod binary
checkmodule -M -m -o myapp_policy.mod myapp_policy.te
# Step 2: Package the .mod into a loadable .pp file
semodule_package -o myapp_policy.pp -m myapp_policy.mod
# Step 3: Install the policy module
semodule -i myapp_policy.pp
# Verify the module is loaded
semodule -l | grep myapp_policy
# Remove a module if needed
semodule -r myapp_policy
Policy modules are stored in /etc/selinux/targeted/modules/active/modules/. The semodule command rebuilds the active policy store after loading or removing modules, which may take a few seconds.
Step 5: Using semanage fcontext for Persistent File Labels
When your application uses a non-standard directory (e.g., /opt/myapp/data/ instead of /var/lib/myapp/), you must define a persistent file context mapping so that files in that directory get the correct SELinux label when created or after a relabel:
# Add a persistent file context mapping
semanage fcontext -a -t httpd_sys_content_t "/opt/myapp/web(/.*)?"
# Apply the context to existing files
restorecon -Rv /opt/myapp/web/
# Verify the new contexts
ls -Z /opt/myapp/web/
# List all custom file context mappings
semanage fcontext -l | grep myapp
Without semanage fcontext, using chcon to set contexts directly on files will be reverted the next time restorecon is run or the filesystem is relabeled at boot.
Step 6: Using semanage port for Custom Ports
If your application listens on a non-standard port, SELinux will deny the name_bind operation. Use semanage port to associate the port with an existing type:
# Allow Apache to bind to port 8080
semanage port -a -t http_port_t -p tcp 8080
# Allow a custom application to bind to port 9999
semanage port -a -t myapp_port_t -p tcp 9999
# List all port label assignments
semanage port -l | grep -E "http|9999"
# Modify an existing mapping
semanage port -m -t http_port_t -p tcp 8443
# Delete a custom port mapping
semanage port -d -p tcp 9999
Step 7: Managing SELinux Booleans
SELinux booleans are on/off switches that enable or disable specific pre-written policy rules without requiring a custom module. RHEL 7 ships with hundreds of booleans for common application behaviors:
# List all booleans with descriptions
semanage boolean --list
# Search for relevant booleans
semanage boolean --list | grep httpd
getsebool -a | grep ftp
# Check the state of a specific boolean
getsebool httpd_can_network_connect
# Set a boolean temporarily (revert on reboot)
setsebool httpd_can_network_connect on
# Set a boolean persistently
setsebool -P httpd_can_network_connect on
setsebool -P httpd_enable_homedirs on
# View booleans changed from default
semanage boolean -l | grep "on off"
Common useful booleans include httpd_can_network_connect_db (allow Apache to connect to databases), ftpd_anon_write (allow anonymous FTP uploads), and ssh_use_tcpd (allow SSH to use TCP wrappers).
Step 8: Using audit2why for Advanced Debugging
When an AVC denial does not have an obvious fix, audit2why and sealert provide deeper analysis:
# Analyze a specific AVC denial from the audit log
ausearch -m avc -ts recent | audit2why
# Use sealert for full HTML-formatted analysis reports
sealert -a /var/log/audit/audit.log > /tmp/selinux_report.html
# Check for silently denied operations using dontaudit rules
# Generate a permissive version of the policy to see all denials including dontaudited ones
semodule -DB # Disable dontaudit rules temporarily
ausearch -m avc | audit2why
semodule -B # Re-enable dontaudit rules
Mastering SELinux policy development on RHEL 7 transforms mandatory access control from an obstacle into a powerful security tool. By following the audit-allow-compile-test cycle, using semanage for persistent context and port assignments, and leveraging booleans for common scenarios, you can confine virtually any application without disabling SELinux. The result is a system where even a fully compromised application process is limited to exactly the files, ports, and resources it legitimately needs — dramatically containing the blast radius of any security incident.