The moment SELinux says no
You copy a configuration file into /etc/nginx/ and restart the service. The service fails immediately. The logs show a permission denied error, but the file permissions look correct. You check ls -l and everything matches. The real blocker is invisible to standard Linux tools. SELinux is enforcing a policy that says this file belongs in a different security domain. This happens every time you move files outside their expected directories or run services from non-standard paths. A botched label change can leave a critical service unable to read its own configuration. Run the verification steps below before you assume the filesystem is broken.
What the type field actually controls
SELinux attaches a security label to every object on the filesystem. The label contains four fields: user, role, type, and level. The type field dictates what processes can access the object and how. Think of the type as a strict job description. A web server process only reads files labeled for web content. A database process only touches files labeled for database storage. If you drop a database file into a web directory, the web server process will refuse to read it, even if the Unix permissions grant full access. The kernel enforces this boundary before the filesystem driver even checks the traditional read, write, and execute bits.
The type field is the core of Fedora's default policy. Every package that ships with Fedora declares which types its files and processes should carry. The policy database maps paths to types, maps processes to domains, and defines exactly which domains can interact with which types. When you install a package with dnf, the post-install scripts run restorecon to apply the correct labels. When you manually create a directory or copy files between unrelated paths, you step outside that mapping. The kernel blocks the access and logs the denial.
Convention aside: Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/. SELinux enforces this boundary strictly. If you modify a file in /usr/lib/, the package manager will overwrite it on the next update, and the security label will revert to the package default.
Read the type field first. Ignore it and you will fight the kernel forever.
Reading the labels on your system
You need to see these labels to understand why a service is failing. The standard ls command hides them by default. You must ask for the security context explicitly. The same applies to running processes and network ports. SELinux does not just protect files. It isolates processes and restricts which ports they can bind to.
Here is how you inspect file labels, process domains, and port assignments on a running system.
# Show the security context alongside standard file permissions
# The fourth column displays user:role:type:level
ls -Z /etc/nginx/
# List running processes with their SELinux domains
# The first column shows the process context
ps auxZ | grep nginx
# Check which ports are allowed for web traffic
# semanage reads the compiled policy database
sudo semanage port -l | grep http
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. Run that command immediately after a service fails. The journal will often show the SELinux denial before the application logs its own generic error.
Check the labels before you change permissions. Half the time the fix is a single relabel command.
Fixing broken labels without breaking policy
Copying files with cp preserves the source label. Moving files with mv preserves the label. Both behaviors break SELinux expectations when the destination directory expects a different type. You need to restore the expected label or define a new one. The approach depends on whether the path already exists in the policy database.
Here is how you reset a directory tree to its factory-default security labels.
# Recursively restore default contexts based on compiled policy rules
# -v prints every file that gets relabeled so you can verify the change
# -R walks the directory tree without following symlinks by default
sudo restorecon -Rv /etc/nginx/
restorecon only works for paths that already have a rule in the SELinux policy database. If you create a new directory like /srv/mysite, the policy has no idea what type it should carry. You must register the path first. The policy database lives in /etc/selinux/targeted/contexts/files/file_contexts.local. You should never edit that file directly. Use the management tool instead.
Here is how you register a custom directory and apply the correct label permanently.
# Add a persistent rule mapping the new path to the web content type
# The regex pattern matches the directory and everything inside it
# -a appends the rule to the local policy database
sudo semanage fcontext -a -t httpd_sys_content_t '/srv/mysite(/.*)?'
# Apply the newly registered rule to the actual filesystem
# restorecon reads the database and updates the inode labels
sudo restorecon -Rv /srv/mysite
Never use chcon for permanent fixes. chcon modifies the label on disk directly. The next time you run restorecon or reboot after a policy update, the label reverts to the default. semanage fcontext writes the rule to the policy database. restorecon reads the database and applies it. Trust the database. Manual file edits drift, snapshots stay.
Verify the label change before restarting the service. Run ls -Z on the target file and confirm the type matches the process domain.
When the audit log fills up
When a process tries to access an object with the wrong label, the kernel blocks the operation and writes an Access Vector Cache denial to the audit log. The denial contains the source process, the target object, the requested permission, and the denied operation. You will see output like type=AVC msg=audit(1715423891.123:456): avc: denied { read } for pid=1234 comm="nginx" name="index.html" dev="sda1" ino=56789 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file. The scontext shows the web server domain. The tcontext shows the file is labeled as a user home file. The policy explicitly forbids httpd_t from reading user_home_t.
Here is how you pull recent denials and translate them into plain English.
# Search the audit log for AVC messages generated in the last few minutes
# -ts recent filters by timestamp to avoid scrolling through months of logs
sudo ausearch -m avc -ts recent
# Parse the raw audit output and explain why the policy blocked the action
# audit2why reads the denial and matches it against policy rules
sudo ausearch -m avc -ts recent | audit2why
Convention aside: SELinux denials show up in journalctl -t setroubleshoot with a one-line summary. Read those before disabling SELinux. The troubleshoot daemon often suggests the exact restorecon or semanage command you need. audit2allow can generate a policy module from denials, but use it carefully. It is better to apply the correct context type than to create overly permissive custom policies. A custom module that grants read access to user_home_t for httpd_t opens a wider attack surface than fixing the file location.
Run ausearch first. Read the actual denial before guessing.
Which tool to reach for
Use restorecon when you need to reset a standard directory to its expected security labels. Use semanage fcontext when you are creating a new path that requires a persistent label outside the default policy. Use chcon when you are testing a temporary label change in a disposable environment. Use audit2allow when you have verified the denial is a policy gap and you must generate a custom module. Use ausearch when you need to correlate denials with specific timestamps or process IDs. Use journalctl -xeu <unit> when you want a combined view of service state and recent log lines. Use setenforce 0 only when you are performing emergency troubleshooting on a non-production system and plan to revert immediately.
Trust the package manager. Manual file edits drift, snapshots stay.