The custom path that SELinux refuses to touch
You moved your application data to /srv/myapp/data to keep /home clean. The service starts, then immediately crashes. The logs show a permission denied error. You check the standard Linux permissions with ls -l and they look fine. The user owns the directory. The group has read access. Yet the process cannot read the files. This is SELinux enforcing its policy. You probably tried sudo chcon -R -t httpd_sys_content_t /srv/myapp/data to fix it. It worked for an hour. Then you rebooted, or ran a system update, or executed sudo restorecon -Rv /. The label vanished. The service crashed again. You need a persistent fix that survives reboots, package updates, and automatic relabeling.
What SELinux is actually happening
Standard Linux permissions check three things: user, group, and others. SELinux adds a fourth dimension: the security context. Every file and process carries a label formatted as user:role:type:level. The type field is what matters for file access. When a process tries to open a file, the kernel checks whether the process type is allowed to interact with the file type. If the policy says no, the kernel blocks the operation and logs an AVC denial.
Think of standard permissions as a building's front door. Anyone with a key can enter. SELinux is the internal security guard. Even if you have a key, the guard checks your badge against the room you are trying to enter. If your badge says httpd_t and the room is labeled default_t, the guard stops you. The guard does not care about your Linux user account. It only cares about the type field.
When you run chcon, you are manually handing the guard a temporary pass. The pass works until the next time the building does a security sweep. Fedora runs automatic sweeps during boot and after certain package updates. chcon overrides get wiped out. semanage fcontext writes a permanent rule into the security policy database. restorecon reads that database and applies the correct labels to the filesystem. The combination survives reboots and system maintenance.
The policy database lives in /etc/selinux/targeted/contexts/files/file_contexts.local. Files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/. The semanage command handles the compilation and placement automatically. You do not need to touch the local file manually.
The persistent fix
You need two tools installed by default on Fedora: semanage and restorecon. Both come from the policycoreutils-python-utils package. If you are on a minimal Server install, you might need to install it first.
Here is how to install the required utilities on a stripped-down system.
sudo dnf install policycoreutils-python-utils
# WHY: provides semanage and restorecon on minimal installs
# WHY: dnf handles dependencies automatically
# WHY: avoids manual package downloads that break transactional integrity
Here is how to define a persistent label for a custom directory. Replace the path and type with your actual values.
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/myapp/data(/.*)?"
# WHY: -a adds a new rule to the policy database
# WHY: -t sets the target SELinux type for matching files
# WHY: the regex matches the directory and everything inside it
sudo restorecon -Rv /srv/myapp/data
# WHY: -R applies the label recursively to all contents
# WHY: -v prints each file as it gets relabeled so you can verify
# WHY: restorecon reads the database and fixes the filesystem
The regex syntax in semanage is strict. The (/.*)? pattern is the standard way to match a directory and all its children. Without it, you only label the top-level directory. Files created later inside that directory will inherit the correct label automatically because the parent directory now has the right context. The question mark makes the entire group optional, which covers both the directory itself and any nested paths.
If you need to change an existing rule instead of adding a new one, use the -m flag. The -a flag will create a duplicate entry and cause unpredictable behavior.
sudo semanage fcontext -m -t container_file_t "/var/lib/custom-storage(/.*)?"
# WHY: -m modifies an existing rule instead of creating a duplicate
# WHY: duplicates confuse the policy compiler and restorecon
# WHY: prevents conflicting matches during relabeling
Verify the label stuck
Run ls -Z on the directory. You will see the security context printed before the standard permissions.
Here is how to confirm the relabel applied correctly to your target path.
ls -Z /srv/myapp/data
# WHY: -Z displays the full SELinux security context
# WHY: confirms restorecon applied the policy correctly
# WHY: shows inheritance for newly created files
Look for the type field. It should match the -t value you passed to semanage. If you see unconfined_u:object_r:default_t:s0 or user_home_t, the relabel did not run correctly. Check your regex syntax. Run semanage fcontext -l | grep /srv/myapp to see exactly what the policy database contains. The output shows the regex pattern and the target type. If the pattern looks wrong, delete it with semanage fcontext -d and start over.
Reboot before you debug. Half the time the symptom is gone after a clean boot cycle.
Common pitfalls and what the error looks like
The most frequent mistake is using chcon for anything that needs to survive a reboot. chcon modifies the extended attributes on disk directly. It does not touch the policy database. When restorecon runs, it compares the filesystem against the database and overwrites your manual change. You will spend hours chasing a ghost that disappears every time the system updates.
Another common issue is regex syntax errors. semanage uses a simplified regular expression format. Square brackets, parentheses, and question marks have specific meanings. If you forget to escape a special character or miss the trailing ?, the rule will not match your files. The command will succeed silently. restorecon will run and print nothing. You will assume it worked until the service crashes again. Always test your regex against semanage fcontext -l before running restorecon.
SELinux denials show up in journalctl -t setroubleshoot with a one-line summary. Read those before disabling SELinux. The summary usually tells you exactly which process tried to access which file and what label it expected.
Here is what a typical denial looks like in the audit log.
type=AVC msg=audit(1715432100.123:456): avc: denied { read } for pid=1234 comm="nginx" name="config.yaml" dev="sda1" ino=56789 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0
The tcontext field shows the current label. The scontext shows the process label. The policy blocks httpd_t from reading default_t. You need to change the file to httpd_sys_content_t or httpd_config_t depending on whether it is static data or a configuration file. The tclass=file tells you the operation targets a regular file, not a directory or socket.
If you accidentally label a system directory with the wrong type, you can break package managers or boot scripts. dnf expects certain directories to carry rpm_var_lib_t or var_t. Changing those labels causes rpmdb corruption errors or failed service starts. Always restrict your semanage rules to custom paths outside /usr, /etc, and /var/lib unless you know exactly what you are overriding.
Trust the package manager. Manual file edits drift, snapshots stay.
When to use this vs alternatives
Use semanage fcontext when you need a file or directory to keep a specific SELinux type across reboots and system updates. Use chcon when you are testing a label temporarily in a live terminal and do not care if it persists. Use restorecon when you want to apply the policy database to a directory that has accumulated incorrect labels over time. Use setsebool when you need to toggle a broad policy switch like allowing a web server to connect to a database, rather than changing individual file labels. Use audit2allow when you are writing a custom policy module for a third-party application that ships without an SELinux policy. Stay on the default enforcing mode if you only deviate from the defaults occasionally.