Audit2allow: Translating Kernel AVC Denials, Generating Custom SELinux Policy Modules, and Hardening Access Enforcement in Production
Bleary-eyed, you run through the familiar muscle memory of Linux diagnostics. You inspect the directory permissions on /var/run/telemetry/: ownership belongs squarely to the daemon user, access bits are generous at 0775, and the disk has gigabytes of free space and plenty of available inodes. You even launch the daemon manually under an administrative shell, only to watch it crash with the exact same refusal. Exhausted, staring down an imminent breach of customer service-level agreements, the dark temptation of every weary sysadmin creeps in: why not just type setenforce 0, silence the alarm, and go back to sleep?
[root@node-01 ~]# setenforce 0 # The siren song of compromised infrastructure
Reaching for that switch may buy you an uninterrupted night of sleep, but it does so by dismantling the hostβs entire security perimeter. Turning off enforcement does not repair a misconfiguration; it throws open the castle gates, leaving every process on the machine exposed to lateral compromise and privilege escalation. The failure confronting you is neither a broken application nor a standard permission defect. It is the Security-Enhanced Linux (SELinux) subsystem doing its job at the kernel boundary, quietly blocking an unauthorized interaction that standard file permissions are blind to.
Navigating this strict kernel boundary does not require turning off your defenses or spending weeks decoding arcane security policy languages. Instead, we can translate the kernel's terse rejection log into clear English and surgical security rules using audit2allow.
Before modifying configuration files or writing complex rules, you can immediately ask the system to explain the blockage and suggest a remediation with a single practical command:
ausearch -m avc -ts recent | audit2allow -w
This diagnostic command interrogates the kernel audit log, identifies why the access was refused, and prints an actionable diagnosis in plain languageβoften pointing you to a pre-existing toggle that solves the incident in seconds.
2. What It Does in Plain English
The audit2allow utility functions as a translator and compiler for Linux systems enforcing Mandatory Access Control (MAC). Under SELinux, standard user permissions are not enough; every process, socket, and file is assigned a cryptographic-like security label, and the kernel strictly forbids any interaction unless an explicit rule authorizes it.
When an operation is blocked, the kernel records a dense, cryptic entry known as an Access Vector Cache (AVC) denial into system audit logs. To a human operator, these log lines look like scrambled alphabet soup. The audit2allow tool reads these raw logs, figures out exactly which process domain was trying to touch which target resource, and generates structured Type Enforcement (.te) policy source code. When requested, it will even run this source code through a compilation toolchain to produce binary policy packages (.pp) and inject them straight into the live kernel, resolving permission hurdles without lowering system security.
3. Theoretical Foundations & Architecture
To use audit2allow safely in production environments, it is essential to understand how the Linux kernel makes access decisions, records denials, and ingests compiled security modules.
(/var/log/audit/audit.log)"] A2A["audit2allow"] SourceTE["Source Policy (.te)"] ModFile["Binary Module (.mod)"] PkgFile["Policy Package (.pp)"] ActiveStore["Active Kernel Policy Store"] LAF -->|"Netlink Broadcast"| AuditD AuditD --> A2A A2A -->|"-m flag"| SourceTE A2A -->|"-M flag (Auto-compile)"| PkgFile SourceTE -->|"checkmodule"| ModFile ModFile -->|"semodule_package"| PkgFile PkgFile -->|"semodule -i"| ActiveStore end
The Access Vector Cache (AVC) Subsystem and auditd
SELinux implements the Flask security architecture. Whenever a process (such as an Nginx web server running in the httpd_t security domain) attempts an operation on an object (such as a Unix domain socket labeled unconfined_service_t), the kernel object manager queries the SELinux Security Server to verify whether an explicit allow rule permits that action.
Because evaluating the entire policy graph on every single system call would grind server performance to a halt, the kernel caches previous access decisions in the Access Vector Cache (AVC):
- Cache Hit (Allowed): The operation proceeds instantly in kernel memory with zero userspace overhead.
- Cache Miss or Denial: If the policy does not explicitly permit the tuple of
(source_type, target_type, object_class, permissions), the request is immediately blocked. The AVC constructs an audit event describing the rejection and broadcasts it over Netlink sockets to the userspaceauditd(8)daemon, which writes the event to/var/log/audit/audit.log.
A raw AVC denial log entry appears in the following standardized format:
type=AVC msg=audit(1692482400.412:892): avc: denied { write } for pid=4812 comm="telemetryd" name="telemetry.sock" dev="tmpfs" ino=34120 scontext=system_u:system_r:telemetry_t:s0 tcontext=system_u:object_r:var_run_t:s0 tclass=sock_file permissive=0
The SELinux Policy Module Compilation Lifecycle
The audit2allow command acts as an automated frontend over a multi-stage compilation toolchain maintained by the SELinux Project. Understanding each stage of this pipeline is invaluable when diagnosing compilation or module loading errors:
- Source Generation (
.te):audit2allowinspects the raw denial attributes (scontext,tcontext,tclass, and the denied operations) and writes a human-readable Type Enforcement source file containing module headers, type requirements, and explicitallowrules. - Intermediate Compilation (
.mod): The Type Enforcement source file is compiled into an intermediate binary policy module usingcheckmodule(8):bash checkmodule -M -m -o telemetry_custom.mod telemetry_custom.te - Module Packaging (
.pp): The binary module is packaged together with optional file-context definitions viasemodule_package:bash semodule_package -o telemetry_custom.pp -m telemetry_custom.mod - Policy Store Linking: The
semodule(8)utility injects the compiled package into the active policy store (converting it to Common Intermediate Language / CIL on modern distributions like RHEL 8/9 and Fedora), instantly refreshing the kernel's active AVC decisions.
Operational Risks: Policy Expansion vs. Domain Validation
While audit2allow makes resolving permission errors remarkably easy, it is a strictly mechanical tool: it assumes every logged denial represents legitimate operational behavior. Blindly generating allow rules without understanding what caused them introduces serious security vulnerabilities:
- Type Contamination: Granting a compromised or misconfigured web service write access to generic system types (such as
etc_torvar_run_t) punctures isolation boundaries designed to contain security breaches. - Masking Labeling Faults: Often, the correct solution is not writing a new rule, but fixing mislabeled files using
restorecon(8)or adjusting persistent path definitions viasemanage fcontext. - Boolean Neglect: Many standard workflows are already supported through built-in SELinux Booleans. Writing a custom policy module when a standard boolean already exists introduces administrative debt and drifts away from upstream vendor support.
4. Command Syntax & Essential Mechanics
The audit2allow binary is distributed within the policycoreutils-python-utils (or policycoreutils) package across modern enterprise distributions. It provides several command-line flags to filter, explain, and transform audit logs.
Core Flags Reference
| Flag | Functional Specification |
|---|---|
-a |
Instructs the utility to read all audit records directly from system audit logs or standard input. |
-i <file> |
Designates a specific input file (such as a quarantined audit log) rather than reading standard streams. |
-m <name> |
Generates human-readable Type Enforcement (.te) policy source code without generating binary output. |
-M <name> |
Executes the full compilation toolchain, outputting both the source .te file and the compiled .pp package. |
-R |
Directs the parser to leverage high-level macros from the installed Reference Policy headers. |
-w |
Explains why an interaction was denied in human-readable prose, citing active booleans where applicable. |
-l |
Filters input records to process only those events generated since the last policy reload. |
Quick Start: Diagnostic Inspection
The safest starting point for diagnosing an unexpected denial is combining ausearch(8) with the -w (why) flag of audit2allow:
ausearch -m avc -ts recent | audit2allow -w
Expected Terminal Output
type=AVC msg=audit(1692482910.104:1043): avc: denied { name_connect } for pid=5102 comm="nginx" dest=8080 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:http_cache_port_t:s0 tclass=tcp_socket permissive=0
Was caused by:
The boolean httpd_can_network_connect was set to 0.
Description:
Allow httpd to can network connect
Allow access by executing:
# setsebool -P httpd_can_network_connect 1
This output immediately reveals whether the issue can be resolved with a standard boolean toggle before you consider building custom policy modules.
5. Five Real-World Production Use Cases
Case 1: Triaging Reverse Proxy Socket Denials
Scenario
An Nginx reverse proxy instance fails to forward requests to an internal application backend communicating over a custom Unix domain socket located at /run/gunicorn/gunicorn.sock. Incoming HTTP client requests fail with a 502 Bad Gateway error.
Execution Command
ausearch -m avc -c nginx -ts recent | audit2allow -w
Realistic Terminal Output
type=AVC msg=audit(1692483120.210:1120): avc: denied { connectto } for pid=6104 comm="nginx" path="/run/gunicorn/gunicorn.sock" scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:system_r:unconfined_service_t:s0 tclass=unix_stream_socket permissive=0
Was caused by:
Missing type enforcement allow rule or incorrect target label.
Allow access by executing:
# ausearch -c 'nginx' --raw | audit2allow -M my-nginx
# semodule -X 300 -i my-nginx.pp
Line-by-Line Technical Analysis
type=AVC msg=audit(1692483120.210:1120): Identifies the record as an Access Vector Cache denial with a timestamp and audit event ID (1120).avc: denied { connectto }: Shows the exact system operation that was rejected: an attempt to connect to a stream socket.comm="nginx": Confirms the executable responsible for the request.scontext=system_u:system_r:httpd_t:s0: The security domain of the Nginx process (httpd_t).tcontext=system_u:system_r:unconfined_service_t:s0: The target context of the socket listener. The socket is running underunconfined_service_t, a generic label for custom services missing an explicit policy.tclass=unix_stream_socket: The SELinux object class being accessed.
Actionable Next Steps
An engineer should not blindly generate a module permitting httpd_t to connect to all unconfined sockets. The clean, secure fix is to assign the standard httpd_var_run_t label to the socket directory so Nginx can communicate with it naturally:
semanage fcontext -a -t httpd_var_run_t "/run/gunicorn(/.*)?"
restorecon -Rv /run/gunicorn
Case 2: Compiling a Custom Daemon Policy Package
Scenario
An in-house microservice, telemetryd, runs in its own confined domain telemetry_t. The service needs to create and update socket control files inside a runtime metrics folder /var/run/node_metrics/ labeled var_run_t. The kernel blocks the daemon during initialization.
Execution Command
ausearch -m avc -c telemetryd --raw | audit2allow -M telemetry_runtime
Realistic Terminal Output
******************** IMPORTANT ***********************
To make this policy package active, execute:
semodule -i telemetry_runtime.pp
audit2allow generates the binary package telemetry_runtime.pp and simultaneously produces the human-readable source file telemetry_runtime.te. Inspecting the source reveals:
module telemetry_runtime 1.0;
require {
type telemetry_t;
type var_run_t;
class sock_file { create unlink write };
class dir { add_name remove_name write };
}
#============= telemetry_t ==============
allow telemetry_t var_run_t:dir { add_name remove_name write };
allow telemetry_t var_run_t:sock_file { create unlink write };
Line-by-Line Technical Analysis
module telemetry_runtime 1.0;: Defines the module name and semantic version.require { ... }: The interface block listing existing system types and object classes that must already exist in the base kernel policy for this module to load.allow telemetry_t var_run_t:dir { add_name remove_name write };: Grants thetelemetry_tdomain permission to add, remove, and update entries inside directories labeledvar_run_t.allow telemetry_t var_run_t:sock_file { create unlink write };: Explicitly allows the service to create, delete, and write Unix domain socket files within those directories.
Actionable Next Steps
- Review
telemetry_runtime.tein a text editor to confirm permissions are strictly scoped. - Load the compiled binary module into the active kernel policy store:
bash semodule -i telemetry_runtime.pp - Restart
telemetrydand confirm it initializes cleanly without triggering AVC rejections.
Case 3: Refining Policy with Reference Policy Interfaces
Scenario
A data processing engine, custom_engine, needs to write log messages directly to the system journal via /dev/log and establish outbound database connections to PostgreSQL on port 5432. Compiling raw vector rules produces low-level, brittle policy code that can break during major operating system updates.
Execution Command
ausearch -m avc -c custom_engine --raw | audit2allow -R -m custom_engine_refined > custom_engine_refined.te
Realistic Terminal Output
Viewing the generated custom_engine_refined.te file:
policy_module(custom_engine_refined, 1.0.0)
gen_require(`
type custom_engine_t;
')
########################################
#
# custom_engine_t local policy
#
# Interface macro to permit system logging via syslog / journald
logging_send_syslog_msg(custom_engine_t)
# Interface macro permitting TCP connections to PostgreSQL endpoints
corenet_tcp_connect_postgresql_port(custom_engine_t)
Line-by-Line Technical Analysis
policy_module(custom_engine_refined, 1.0.0): An M4 macro from the SELinux Reference Policy initializing the module structure.gen_require(\... ')`: An M4 macro block asserting external domain dependencies dynamically.logging_send_syslog_msg(custom_engine_t): A high-level interface macro that expands into all required socket, path resolution, and permission rules needed to interact with/dev/log.corenet_tcp_connect_postgresql_port(custom_engine_t): A semantic macro permitting outbound network connections to ports labeledpostgresql_port_t, abstracting away raw port numbers and firewall rules.
Actionable Next Steps
Compile the macro-templated policy source using the system policy development headers provided by the selinux-policy-devel package:
make -f /usr/share/selinux/devel/Makefile custom_engine_refined.pp
semodule -i custom_engine_refined.pp
Case 4: Differential Policy Generation After Staging Deployments
Scenario
During a staging release of an updated container orchestration agent, several test suites trigger AVC denials. The engineer must capture only the security delta introduced by the new test run, filtering out background noise and historical system events.
Execution Command
# Reload policy to establish an audit baseline marker
semodule -R
# Execute the automated integration test suite
/opt/orchestration/bin/run_tests.sh
# Isolate only the denials registered since the baseline reload
audit2allow -l -i /var/log/audit/audit.log -M staging_release_delta
Realistic Terminal Output
******************** IMPORTANT ***********************
To make this policy package active, execute:
semodule -i staging_release_delta.pp
Examining the resulting staging_release_delta.te source:
module staging_release_delta 1.0;
require {
type orchestrator_t;
type cgroup_t;
class file { getattr open read };
}
#============= orchestrator_t ==============
allow orchestrator_t cgroup_t:file { getattr open read };
Line-by-Line Technical Analysis
- The
-lflag instructsaudit2allowto query the kernel for the timestamp of the latest policy reload and ignore all historical entries in/var/log/audit/audit.logrecorded before that moment. allow orchestrator_t cgroup_t:file { getattr open read };: Confirms that the newly tested release exercises code reading kernel Control Group interfaces (cgroup_t), which had not been declared in the service's base profile.
Actionable Next Steps
- Verify with the engineering team that reading
cgroup_tfiles is expected behavior for the new release. - Commit
staging_release_delta.teinto version control alongside the application codebase to track infrastructure security definitions. - Deploy the compiled package across the staging test fleet.
Case 5: Automated Audit Pipeline Integration
Scenario
In a hardened production cluster, platform engineers need an automated script to triage, sanitize, and prepare policy updates for service rejections without setting nodes to permissive mode or disrupting uptime.
Execution Pipeline Script
#!/usr/bin/env bash
set -euo pipefail
MODULE_NAME="remediation_$(date +%Y%m%d_%H%M%S)"
AUDIT_LOG="/var/log/audit/audit.log"
WORKSPACE="/var/local/selinux_triage"
mkdir -p "${WORKSPACE}"
cd "${WORKSPACE}"
echo "[*] Extracting unhandled AVC denials for service: payment_gateway..."
ausearch -ts recent -m avc -c payment_gateway > raw_denials.log || {
echo "[!] No recent denials found for payment_gateway."
exit 0
}
echo "[*] Synthesizing Type Enforcement policy candidate..."
audit2allow -i raw_denials.log -m "${MODULE_NAME}" > "${MODULE_NAME}.te"
echo "[*] Validating that candidate policy does not grant dangerous capabilities..."
if grep -E "(sys_admin|dac_override|dac_read_search)" "${MODULE_NAME}.te"; then
echo "[CRITICAL] Refusing to compile: Policy contains high-risk kernel capabilities!"
exit 1
fi
echo "[*] Compiling policy package..."
checkmodule -M -m -o "${MODULE_NAME}.mod" "${MODULE_NAME}.te"
semodule_package -o "${MODULE_NAME}.pp" -m "${MODULE_NAME}.mod"
echo "[SUCCESS] Policy package ${MODULE_NAME}.pp prepared for administrative review."
Realistic Terminal Output
[*] Extracting unhandled AVC denials for service: payment_gateway...
[*] Synthesizing Type Enforcement policy candidate...
[*] Validating that candidate policy does not grant dangerous capabilities...
[*] Compiling policy package...
checkmodule: loading policy configuration from remediation_20260819_021500.te
checkmodule: policy configuration loaded
checkmodule: writing binary representation (version 19) to remediation_20260819_021500.mod
[SUCCESS] Policy package remediation_20260819_021500.pp prepared for administrative review.
Line-by-Line Technical Analysis
ausearch -ts recent -m avc -c payment_gateway: Extracts only audit records matching thepayment_gatewayprocess within the recent timeframe, preventing unrelated system events from contaminating the policy.grep -E "(sys_admin|dac_override...)": An automated safety check that halts compilation if high-risk bypass capabilities are requested.checkmodule -M -mandsemodule_package: Compiles and packages the verified.tefile into an isolated.ppbundle ready for review.
Actionable Next Steps
Security staff inspect the generated .te source file and deploy the compiled package to production:
semodule -i /var/local/selinux_triage/remediation_20260819_021500.pp
6. Operational Best Practices, Caveats & Failure Modes
The "Permit Everything" Antipattern
The single most dangerous mistake when using audit2allow is executing a global, unfiltered audit ingest:
# ANTI-PATTERN: NEVER RUN THIS IN PRODUCTION
audit2allow -a -M grant_all_denials
If a host has been targeted by vulnerability scanners, suffered an intrusion attempt, or encountered file-system corruption, running audit2allow -a ingests those malicious or abnormal events and turns them into permanent kernel authorizations.
audit2allow -a -M bad_policy"] --> B["Attack Vectors & Maliceβ’ Reverse shell sockets
β’ Tampered binary access"] A --> C["System Misconfigurations
β’ Mislabeled files
β’ Permission leaks"] B --> D["Codified Into Permanent Kernel Policy (.pp)
β’ Complete Mandatory Access Control Collapse"] C --> D
Always isolate audit logs by process command name (-c), message type, or time window using ausearch before passing data to audit2allow.
Auditing .te Files for Critical Capabilities
Before loading any generated module into a running kernel, always check the .te source file for high-risk capabilities:
dac_override/dac_read_search: Indicates that the process tried to bypass standard Linux file ownership checks. Authorizing this capability disables discretionary access control protections for that service.capability sys_admin: Grants broad administrative powers across kernel interfaces, creating a near root-equivalent security domain.entrypoint/execute_no_trans: Altering execution transitions can allow a confined application to execute system binaries without standard security domain boundaries.
If audit2allow produces rules requesting these capabilities, investigate why the application requested them at the code level rather than granting the permission in SELinux.
Verifying Module Activation and Resolving Collisions
To confirm that a newly compiled policy module is active in the running kernel:
# List active modules (Priority 400 represents custom administrator overrides)
semodule -lfull | grep telemetry
Output
400 telemetry_runtime pp
Handling Module Collisions and Dependency Faults
If a new policy module conflicts with an existing type definition or upstream package, semodule will abort the installation:
libsepol.scope_copy_callback: telemetry_runtime: Duplicate declaration in module: type telemetry_t (No such file or directory).
libsemanage.semanage_link_sandbox: Link packages failed
semodule: Failed!
To resolve this issue:
1. Verify whether the type is already defined by inspecting active kernel types:
bash
seinfo -t telemetry_t
2. If the type is already declared in the base policy, place it inside the require { ... } block of your custom .te file rather than redeclaring it at the top level of the module.
7. Today's Takeaway
The next time an application on a secured Linux server fails with a cryptic Permission denied error, step away from the temptation of setenforce 0. Instead, open your terminal and run ausearch -m avc -ts recent | audit2allow -w. In less than five minutes, you will convert what looked like an impenetrable kernel barrier into an intelligible diagnosisβrevealing whether a simple boolean toggle or a clean, two-line Type Enforcement rule can restore your service while keeping your host's defenses intact.