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 (getenforce returns Enforcing)
  • 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.