You see a denial, but permissions look fine
You start a service, run a container, or execute a script, and the application crashes immediately. The application logs show Permission denied, but you check the file with ls -l and the permissions are wide open. The user is correct. The group is correct. The owner is correct. Yet the operation fails.
The culprit is SELinux. You check the system logs and find lines starting with avc: denied. The system is working exactly as designed. SELinux has caught a process trying to perform an action that the policy does not explicitly allow. This is not a bug. This is the mandatory access control layer doing its job. Your workload is requesting access that falls outside the defined boundaries.
What SELinux is actually doing
Linux file permissions control access based on user and group identity. That is discretionary access control. The file owner decides who gets in. SELinux adds mandatory access control. It decides access based on labels attached to both the process and the object. Even if the file permissions allow access, SELinux can block it if the labels do not match the policy rules.
Think of file permissions as the lock on the front door. SELinux is the security guard inside the building checking your badge. You can pick the lock, but the guard stops you if your badge does not match the room you are trying to enter. Every process runs with a type label. Every file has a type label. The policy defines which process types can perform which actions on which file types. If there is no rule allowing the action, SELinux denies it and logs the event.
Labels follow the format user:role:type:level. The type field is what matters for most denials. You will see types like httpd_t for the web server, user_home_t for home directories, and container_t for containers. When you see a denial, you are seeing a mismatch between the type of the process and the type of the resource it is trying to access.
Check the label with ls -Z. If the type is wrong, no amount of chmod will help.
Read the denial before you fix it
SELinux denials generate detailed messages in the journal. Fedora includes the setroubleshoot service, which analyzes raw AVC messages and produces human-readable summaries. These summaries often suggest the exact fix, such as a boolean to toggle or a file context to correct. Ignoring the summary and jumping straight to custom policy generation creates unnecessary security holes.
Here's how to find the relevant denial messages and read the troubleshooting summary.
# WHY: -t filters by tag, -e jumps to the end of the log, -p info limits output to informational messages.
journalctl -t setroubleshoot -e -p info
The output will contain a Summary section. It might say something like httpd can be allowed to access user home directories via the httpd_enable_homedirs boolean. If you see that, you do not need a custom module. You need to toggle a boolean. The summary also includes a Detailed Description explaining why the denial occurred and a Allow Access section with the recommended command.
Run journalctl first. Read the actual error before guessing.
Fix the label, not the policy
Most denials happen because a file ended up in a directory with the wrong default label. For example, you copied a web page into /var/www/html, but the file retained the label from your home directory. The web server tries to read it, sees the wrong label, and denies access.
The fix is to correct the label. Many users run chcon to change the label immediately. This works temporarily. The label changes vanish after a filesystem relabel or a reboot. The correct approach is to update the file context database so the label persists.
Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/. SELinux policy follows the same convention. Custom contexts go into the policy database, not the base policy files.
Here's how to add a persistent context rule and apply it to the filesystem.
# WHY: -a adds a new rule, -t sets the target type, the regex matches the path and all contents.
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/html(/.*)?"
# WHY: -R applies recursively, -v shows verbose output, applies context rules from the database to the filesystem.
sudo restorecon -Rv /var/www/html
The semanage fcontext command updates the policy database. The restorecon command reads that database and fixes the labels on disk. This ensures the labels survive reboots and package updates.
Use semanage for persistence. chcon disappears on relabel.
Generate a custom module when labels aren't enough
Sometimes the denial is not about a wrong label. The application might need to access a resource that the base policy does not anticipate, or the interaction is complex enough that booleans and contexts cannot cover it. In those cases, you generate a custom policy module. This module adds a specific allow rule for the denied access.
Generating a module requires the setools-console package. It provides audit2allow, which reads audit logs and produces policy source code.
Here's how to install the tools needed for policy analysis and generation.
# WHY: setools-console provides audit2allow, seinfo, and sesearch for analyzing and generating policy.
sudo dnf install setools-console
Before generating the module, extract the relevant denial messages. The ausearch command queries the audit log. You want to filter for AVC messages from the recent past to avoid including old, resolved denials.
Here's how to search for recent AVC denials and pipe them to the policy generator.
# WHY: -m avc filters access vector cache messages, -ts recent limits to the last hour, -M names the module.
sudo ausearch -m avc -ts recent | audit2allow -M myfix -o myfix.te
The command creates two files: myfix.te and myfix.pp. The .te file is the source code. The .pp file is the compiled policy package. You inspect the .te file to verify the rule is safe. You install the .pp file.
Here's what the generated policy source looks like.
# WHY: This defines the module name and version, and lists the required types and classes.
module myfix 1.0;
require {
type httpd_t;
type user_home_t;
class file read;
}
# Allow httpd to read files labeled user_home_t.
allow httpd_t user_home_t:file read;
Review the allow line carefully. Does it grant exactly the access you need? If it grants write or execute when you only need read, edit the .te file to restrict it. audit2allow is permissive by default. It grants everything in the log. You must narrow the scope.
Here's how to install the compiled module into the running system.
# WHY: -i installs the policy package, loading the rules into the kernel immediately.
sudo semodule -i myfix.pp
The module loads instantly. No reboot is required. The service can retry the operation immediately.
Snapshot the system before loading a custom module. A bad module can break other services.
Verify the fix
After loading the module, confirm it is active and check for new denials. The semodule -l command lists all loaded modules. You should see your custom module in the list. Run the failing operation again. Check the journal for new setroubleshoot messages. If the denial is gone and the application works, the fix is successful.
Here's how to verify the module is loaded and check for residual errors.
# WHY: Lists loaded modules, grep filters for your module name to confirm installation.
semodule -l | grep myfix
# WHY: Checks the journal for new denials after the fix was applied.
journalctl -t setroubleshoot -e -p info
If you see new denials, the fix was incomplete. The application might be accessing additional resources. Repeat the analysis. Generate a new module or refine the existing one. Do not stack modules blindly. Merge rules into a single module for easier management.
Check for new denials immediately. A fix can mask a deeper misconfiguration.
Common pitfalls and what the error looks like
Blindly running audit2allow on all denials is the most common mistake. This opens the system to any access the application requested, including access to sensitive files. Always review the generated policy. Restrict the rules to the minimum required access.
Another pitfall is forgetting the require block. If the .te file references a type that does not exist in the policy, the compilation fails. You will see an error like Error: Could not create policy module file. Check the require block. Ensure all types and classes are listed.
If you see semodule: Failed to install policy module, the module might conflict with an existing rule or contain a syntax error. Check the .te file for typos. Verify the types exist using seinfo -t.
SELinux denials show up in journalctl -t setroubleshoot with a one-line summary. Read those before disabling SELinux. Disabling SELinux removes the protection layer entirely. It is never the correct fix for a denial.
If the module fails to load, check the require block. Missing types break the compile.
When to use which tool
Use setsebool when the denial involves a standard service behavior that Fedora already supports via a boolean toggle. Use semanage fcontext and restorecon when the file exists in a directory with the wrong default label and you need to fix the label permanently. Use audit2allow when the application requires access that no boolean or file context change can cover, and you have verified the access is safe. Use getenforce 0 only for temporary debugging on a non-production system, and revert immediately after.