You compiled a web service and Fedora refused to serve it
You drop a custom application into /var/www/html, set the traditional permissions to 755, and restart Apache. The browser returns a 403 Forbidden. You check firewall-cmd and the port is open. You check ls -l and the owner is correct. Then you glance at the journal and see a line about httpd_t being denied access to a file labeled user_home_t. You assume the package manager broke something or Fedora is being unnecessarily strict. It is not. SELinux is doing exactly what it was designed to do.
What's actually happens
Traditional Linux permissions rely on discretionary access control. The owner of a file decides who can read or write it. If a process runs as root, it inherits that authority and can touch almost anything on the disk. SELinux replaces that model with mandatory access control. The kernel enforces a policy that dictates what every process is allowed to do, regardless of the user account running it.
Think of traditional permissions as a building with a keycard reader. If you have the right card, you enter. SELinux is a security detail with a detailed itinerary. Even if you have the right card, you cannot enter the server room, you cannot touch the fire suppression system, and you cannot leave through the loading dock. The policy defines the itinerary. Fedora ships with the targeted policy by default. This policy confines network-facing services like Apache, SSH, Docker, and PostgreSQL while leaving most user-space applications unconfined. The goal is to limit the blast radius when a service gets compromised. If an attacker exploits a vulnerability in your web server, SELinux stops them from reading /etc/shadow, modifying system binaries, or pivoting to the rest of the machine.
SELinux tracks three main attributes on every file and process: user, role, and type. The type is the most important for daily administration. A type like httpd_sys_content_t tells the kernel that a file belongs to the web server and can only be touched by processes labeled httpd_t. A type like bin_t marks an executable. When a process tries to open a file, the kernel checks the policy matrix. If the source type and target type do not have an allowed rule, the kernel blocks the operation and writes a denial to the audit log.
Convention aside: journalctl -xe reads better than journalctl alone. The x flag adds explanatory text and the e flag jumps to the end. Most sysadmins type journalctl -xeu <unit> muscle-memory style. SELinux denials show up in journalctl -t setroubleshoot with a one-line summary. Read those before disabling SELinux.
The fix or how-to
When SELinux blocks an action, it logs the denial and stops the operation. The first step is always to read the log. Fedora includes setroubleshoot, which translates raw audit messages into plain English and suggests corrective commands.
Run this command to see recent denials:
sudo journalctl -t setroubleshoot -n 20 --no-pager # WHY: pulls the last 20 SELinux denial summaries without paging
The output will contain a line starting with SELinux is preventing. It will list the source process, the target file, and the suggested fix. Most of the time, the fix is a single command to restore the correct security context or generate a local policy module.
If you moved a file to a new location, the old security label travels with it. The kernel does not automatically relabel files when you copy or move them. You need to tell the system to apply the correct context based on the file's new path.
sudo restorecon -v /var/www/html/myapp # WHY: reads the default policy for that path and reapplies the correct label
For custom applications that need access to non-standard directories, you can generate a policy module instead of disabling enforcement. The audit2allow tool reads the denial logs and writes a policy file.
sudo ausearch -c 'myapp' --raw | audit2allow -M myapp-policy # WHY: extracts denials for the command and compiles them into a loadable module
sudo semodule -i myapp-policy.pp # WHY: installs the module into the running kernel policy
If you are debugging a broken service and need to see what would happen without the restrictions, switch to permissive mode. This mode logs denials but allows the actions to proceed.
sudo setenforce 0 # WHY: toggles the runtime mode to permissive without rebooting
Reboot before you debug. Half the time the symptom is gone.
Verify it worked
After applying a context fix or loading a policy module, restart the affected service and check the journal again.
sudo systemctl restart myapp.service # WHY: forces the service to pick up the new policy state
sudo journalctl -u myapp.service -n 5 --no-pager # WHY: shows the latest five log lines to confirm clean startup
Verify the security context matches the policy expectation:
ls -Z /var/www/html/myapp # WHY: displays the SELinux label alongside standard permissions
The output should show a type like httpd_sys_content_t or bin_t depending on the file's purpose. If the label matches the documentation and the service starts without errors, the configuration is correct. You can also check the global enforcement state to confirm the system is running as expected.
getenforce # WHY: prints Enforcing, Permissive, or Disabled in a single line
Run journalctl first. Read the actual error before guessing.
Common pitfalls and what the error looks like
The most common mistake is disabling SELinux entirely because a single denial appeared. Editing /etc/selinux/config and setting SELINUX=disabled requires a full reboot to take effect. During that reboot, the filesystem is not relabeled. This leaves thousands of files with missing or incorrect security contexts. The system will boot into a broken state where services refuse to start and desktop environments fail to load. Always use setenforce 0 for temporary testing. If you must change the persistent configuration, run sudo touch /.autorelabel before rebooting so the system relabels the entire disk on startup.
If you see Permission denied in a script but ls -l shows rwxr-xr-x, check the SELinux context first. Traditional permissions and SELinux contexts are independent. A file can have open traditional permissions but still be blocked by a restrictive policy type. This happens frequently when users move configuration files from their home directory to /etc/. The files retain the user_home_t label, and system services refuse to read them.
Another frequent issue is mixing up runtime changes and persistent configuration. setenforce 0 only lasts until the next reboot. The file /etc/selinux/config controls the default mode on boot. Always verify both if you are troubleshooting a recurring issue. Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/.
If you are running Docker or Podman, container images often contain files with generic labels. The container runtime handles most relabeling automatically, but bind mounts from the host bypass that safety net. Mounting a host directory into a container without the :Z or :z option will trigger denials when the container tries to write to it. Add the correct mount option or apply restorecon to the host path before starting the container.
Trust the package manager. Manual file edits drift, snapshots stay.
When to use this vs alternatives
Use Enforcing mode when you want the kernel to actively block unauthorized access and log every violation. Use Permissive mode when you are testing a new application or writing a custom policy and need to see what would be blocked without interrupting service. Use Disabled mode only when you are running legacy software that fundamentally breaks under mandatory access control and you have isolated the system from untrusted networks. Stay on the default targeted policy if you are running standard Fedora workloads.