SELinux Mandatory Access Control (MAC) confines processes to only the resources they legitimately need, limiting the damage an attacker can do even after exploiting a vulnerability. While RHEL 8 ships with broad pre-built policies for common services, custom applications often generate AVC (Access Vector Cache) denial messages because no policy exists for them yet. Rather than setting SELinux to permissive mode — which defeats its purpose — the correct approach is to capture those denials, generate a targeted policy module, and load it. In this tutorial you will work through the full lifecycle: capturing AVC denials, generating and loading a policy module with audit2allow, writing a custom .te type-enforcement file manually, and querying the policy with seinfo and sesearch.
Prerequisites
- RHEL 8 server with a non-root sudo user
- SELinux in enforcing mode (
getenforcereturnsEnforcing) - A custom application installed and producing AVC denials in
/var/log/audit/audit.log - Packages:
policycoreutils-python-utils,setools-console,policycoreutils-devel
Step 1 — Install SELinux Policy Development Tools
dnf install -y policycoreutils-python-utils
setools-console
policycoreutils-devel
audit
# Confirm auditd is running (it must be for AVC logging)
systemctl enable --now auditd
systemctl status auditd
Ensure your target application is running and generating denials. Set the domain to permissive temporarily if it is crashing before you can capture enough denials:
# Find the SELinux type/domain of your process
ps -eZ | grep myapp
# Temporarily set only that domain to permissive (not the whole system)
semanage permissive -a myapp_t
Step 2 — Capture AVC Denials
Use ausearch to extract recent AVC messages from the audit log. Pipe the output to a file so you can feed it to audit2allow.
# Show all AVCs from the last boot
ausearch -m AVC -ts boot | tail -50
# Show AVCs from the last 10 minutes
ausearch -m AVC -ts recent
# Save denials for a specific executable to a file
ausearch -m AVC -ts today -c myapp > /tmp/denials.txt
cat /tmp/denials.txt
Each AVC record shows the source type (the process domain), the target type (the resource), the class (file, socket, etc.), and the permissions denied. Understanding these fields is essential before generating a policy — approve only what your application legitimately requires.
Step 3 — Generate a Policy Module with audit2allow
audit2allow reads AVC denial records and outputs SELinux allow rules, then compiles them into a loadable binary policy package (.pp).
# Preview what rules would be generated (human-readable)
audit2allow -a -i /tmp/denials.txt
# Generate a named policy module: myapp.te (type enforcement) + myapp.mod + myapp.pp
audit2allow -M myapp -i /tmp/denials.txt
# Review the generated .te file before loading
cat myapp.te
# Load the compiled policy package into the kernel
semodule -i myapp.pp
# Verify the module is loaded
semodule -l | grep myapp
Always review myapp.te before loading. Auto-generated rules are a starting point — remove any rules that seem overly broad (e.g., allowing { read write } on etc_t) and re-compile manually.
Step 4 — Write a Custom Type Enforcement File Manually
For production use, writing the .te file by hand gives precise control. A minimal custom policy module looks like this:
cat > /root/myapp.te << 'EOF'
policy_module(myapp, 1.0)
require {
type myapp_exec_t;
type myapp_t;
type var_log_t;
type net_conf_t;
class file { read open getattr };
class dir { search getattr };
}
# Allow myapp to read its own log directory
allow myapp_t var_log_t:dir { search getattr };
allow myapp_t var_log_t:file { read open getattr };
# Allow myapp to read /etc/resolv.conf
allow myapp_t net_conf_t:file { read open getattr };
EOF
# Compile the .te file into a .mod and then a .pp
checkmodule -M -m -o myapp.mod myapp.te
semodule_package -o myapp.pp -m myapp.mod
# Load it
semodule -i myapp.pp
semodule -l | grep myapp
Increment the version number in policy_module(myapp, 1.0) each time you update and reload the module, otherwise semodule will warn about the version.
Step 5 — Query the Policy with seinfo and sesearch
seinfo and sesearch let you inspect what the loaded policy allows without reading raw source files.
# List all types matching a pattern
seinfo -t | grep myapp
# Show all allow rules where myapp_t is the source domain
sesearch --allow -s myapp_t
# Show allow rules for a specific target type and class
sesearch --allow -s myapp_t -t var_log_t -c file
# Show all roles that can transition to myapp_t
seinfo -r | xargs -I{} sh -c 'sesearch --allow -s {} -t myapp_t 2>/dev/null | grep -q . && echo {}'
# Show all type transitions from init_t (what domains init can start)
sesearch --type_trans -s init_t | grep myapp
Step 6 — Persistent Label Changes with semanage
File context changes made with chcon are reset by restorecon or a relabel. Use semanage fcontext for persistent customizations.
# Add a persistent file context rule
semanage fcontext -a -t myapp_exec_t "/opt/myapp/bin(/.*)?"
# Apply the new context to existing files
restorecon -Rv /opt/myapp/bin/
# Verify
ls -Z /opt/myapp/bin/myapp
# Remove the permissive exception added in Step 1 (now enforcing for real)
semanage permissive -d myapp_t
getenforce
Conclusion
You have captured AVC denials with ausearch, auto-generated a policy module with audit2allow, manually crafted a production-quality .te file, loaded both via semodule, and verified the active policy with seinfo and sesearch. Working through this cycle ensures your applications run confined under SELinux enforcing mode without resorting to the dangerous setenforce 0 workaround.
Next steps: How to Configure SELinux Booleans for Common Services on RHEL 8, How to Audit File Access with auditd on RHEL 8, and How to Install HashiCorp Vault for Secrets Management on RHEL 8.