How to Create Custom SELinux Policy Modules on Fedora

Create a custom SELinux policy module by writing rules in a .te file, compiling it with checkmodule, packaging it with semodule_package, and loading it with semodule.

You compiled a custom daemon and it refuses to run

You wrote a Python script or compiled a C binary that needs to read a configuration directory outside its home folder. You run it, and it immediately exits with a permission error. journalctl -xe shows a wall of AVC denials. Disabling SELinux works, but it defeats the entire security model and leaves the system exposed. You need a surgical fix that grants exactly the permissions your application requires while leaving the rest of the system locked down.

What's actually happening

SELinux does not rely on a single monolithic configuration file. The base policy ships with the kernel and the selinux-policy package. It defines thousands of rules for standard services like httpd_t, sshd_t, and user_t. You never edit the base policy directly. Manual edits vanish during updates and break the policy hash. Instead, you write custom policy modules. Think of the base policy as a building's permanent security architecture. Custom modules are temporary access badges issued to specific contractors. They compile into a binary format, load into the kernel alongside the base policy, and can be removed without touching the core system.

The pipeline moves through three stages. A human-readable type enforcement file (.te) defines the rules. A compiled module (.mod) translates those rules into a binary intermediate format. A packaged policy file (.pp) wraps the binary with metadata headers that the kernel understands. The semodule command installs the package into the persistent policy store. The kernel merges it with the base policy at boot and applies it immediately.

Config files in /etc/selinux/targeted/ are user-modified. Files in /usr/share/selinux/ ship with the package. Edit /etc/. Never edit /usr/share/. The package manager controls the upstream directories. Custom modules live in /etc/selinux/targeted/modules/active/ and survive system updates.

Run journalctl -t setroubleshoot first. Read the actual denial before guessing.

The fix

Start by isolating the exact denial. Run your application and watch the audit log. The setroubleshoot package translates raw AVC messages into readable summaries. Most sysadmins type journalctl -xeu <unit> muscle-memory style, but for SELinux denials the setroubleshoot tag cuts through the noise.

# Capture recent SELinux denials and jump to the end of the log
sudo journalctl -t setroubleshoot -e

The output will list the denied operation, the source domain, and the target file context. Copy the denial or note the source command. You will feed this into audit2allow, which parses audit logs and generates policy rules. Install the policy development tools first. dnf upgrade --refresh is the normal weekly maintenance command, but you need the -devel package for this workflow.

# Provides audit2allow, checkmodule, and semodule_package
sudo dnf install selinux-policy-devel

Generate the type enforcement file. Pipe the recent audit log directly into the tool. The -M flag names the module, and -o writes the output file. audit2allow reads the avc messages and extracts the minimum permissions required.

# Parse the last 500 lines of the audit log and generate a .te file
sudo ausearch -m avc -ts recent | audit2allow -M myapp -o myapp.te

Open myapp.te in a text editor. The file contains a module declaration, a require block for external types, and an allow rule. Review the allow line carefully. audit2allow grants the minimum permissions it observed, but it does not understand application logic. If your script needs to write to a log file later, the rule will not include it until the next denial occurs.

module myapp 1.0;

require {
    type myapp_t;
    type var_log_t;
    class file { read open getattr };
}

# Allow the application domain to read log files
allow myapp_t var_log_t:file { read open getattr };

Compile the text file into a binary module. The -M flag enables MLS/MCS support, and -m specifies a module rather than a base policy. The compiler validates syntax and resolves type dependencies.

# Compile the .te file into a loadable .mod binary
checkmodule -M -m -o myapp.mod myapp.te

Package the compiled module. The kernel requires a specific archive format that includes the module binary and a metadata header. The package format ensures the kernel can verify the policy version before loading.

# Wrap the .mod file into a .pp package for the kernel
semodule_package -o myapp.pp -m myapp.mod

Load the package into the running system. The semodule command handles the installation and updates the policy store. The kernel applies the new rules immediately without a reboot.

# Install the package into the active SELinux policy
sudo semodule -i myapp.pp

Run your application again. The denial should disappear.

Compile the module on the target machine. Cross-compiled packages fail verification.

Verify it worked

Confirm the module is active and check the logs for clean execution. The semodule command maintains a list of loaded modules in the policy store.

# List loaded modules and filter for your custom name
semodule -l | grep myapp

The command returns myapp 1.0. Run the application and check the journal one more time.

# Verify no new AVC denials appeared for your process
sudo journalctl -t setroubleshoot -n 20

If the log is empty or shows unrelated messages, the policy is working. Reboot the system to ensure the module persists across kernel updates. Custom modules survive reboots by default, but a clean boot cycle validates the persistent policy store. systemctl status <unit> shows recent log lines AND state in one view. Always check status before restart.

Reboot before you debug. Half the time the symptom is gone.

Common pitfalls and what the error looks like

The compilation step fails when the .te file references undefined types. You will see the following error in the terminal:

checkmodule:  error(s) encountered while parsing configuration
error(myapp.te, 4): unknown type myapp_t

The error points to a specific line and type name. Cross-reference the missing type with seinfo -t or add it to the require block. audit2allow usually handles this, but manual edits break the dependency chain. SELinux denials show up in journalctl -t setroubleshoot with a one-line summary. Read those before disabling SELinux.

Loading fails when the module targets a different policy version than the running system. The kernel prints libsemanage.semanage_direct_install_info: Running restorecon after install failed. This happens when you copy a .pp file from a different Fedora release or a different architecture. Always compile modules on the target machine. Fedora's release cadence is 6 months. The N-2 release goes EOL when N+1 ships. Plan upgrades on that cycle.

File context denials persist even after the module loads. SELinux checks both process permissions and file labels. If your application creates new files, they inherit the process context, not the target directory context. You must define file context rules in the .te file and apply them with semanage fcontext and restorecon. A missing file context rule causes repeated denials on the same path.

Over-permissive rules hide future misconfigurations. Granting allow myapp_t *:file *; works immediately but removes the security boundary. Stick to the specific classes and permissions audit2allow generates. Iterate when new denials appear. firewall-cmd --reload after every rule change. Otherwise the runtime config and the persistent config diverge. The same principle applies to SELinux policy. Keep the active store and the disk store synchronized.

Trust the package manager. Manual file edits drift, snapshots stay.

When to use this vs alternatives

Use custom policy modules when you need persistent, auditable permissions for a custom daemon or script. Use setsebool when the base policy already provides a toggle for a specific feature like FTP home directories or Samba shares. Use restorecon when files have incorrect labels after a manual copy or backup restore. Use audit2allow directly in a pipeline when you want to generate a rule without saving intermediate files. Stay on the default base policy if your application only needs standard user permissions.

Run journalctl first. Read the actual error before guessing.

Where to go next