Manage SELinux Modes and File Contexts

Tested on RHEL 10.2 (Coughlan) — vm1.lab.example
Package policycoreutils 3.10-1.el10
policycoreutils-python-utils 3.10-1.el10
selinux-policy-targeted 42.1.18-4.el10_2.1
httpd 2.4.63-13.el10_2.5 (lab scenario)
Applies to RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora
Privilege Normal user for read-only checks; sudo for setenforce, semanage, restorecon, and service tests
Scope MAC concepts, SELinux modes, getenforce / setenforce, /etc/selinux/config, contexts on files and processes, matchpathcon, restorecon, semanage fcontext, AVC search, move versus copy labeling, and an httpd /srv/site fix. Does not cover port labels, booleans, custom policy modules, or Ansible automation depth.
Related guides Linux file permissions
Disable SELinux
SELinux ports and booleans
Ansible SELinux file context
RHCSA tutorial
IMPORTANT
This guide covers SELinux modes and filesystem contexts—the workflow when a service cannot access a file: inspect the label, compare policy expectations, add a persistent mapping, and restorecon. Port labels and booleans live in SELinux ports and booleans. Disabling SELinux is documented only because users search for it; RHEL recommends permissive troubleshooting instead.

SELinux adds Mandatory Access Control (MAC) on top of owner, group, and mode bits. When httpd returns 403 on a readable file, the failure may be SELinux—not missing chmod. This walkthrough uses httpd serving /srv/site on vm1.lab.example with the targeted policy.


What Is SELinux?

SELinux is a MAC layer the kernel enforces after traditional DAC (owner, group, rwx) checks.

Concept Role
Policy Rules that define allowed domain-to-type access
Process domain (type) Label on a running process, such as httpd_t
Object type Label on files, directories, sockets, and ports
Type Enforcement Most targeted-policy decisions hinge on process type versus object type

If DAC denies an operation, you see a normal permission error and SELinux may not log an AVC. When DAC allows access but policy forbids it, look for SELinux denials. DAC basics live in Linux file permissions.


SELinux Quick Reference

Task Command
Current mode getenforce
Detailed status sestatus
Temporary permissive sudo setenforce 0
Temporary enforcing sudo setenforce 1
File context ls -Z FILE
Process context ps -eZ
Expected context matchpathcon PATH
Restore default label sudo restorecon -v PATH
Add persistent mapping sudo semanage fcontext -a -t TYPE 'PATH(/.*)?'
Apply mapping sudo restorecon -Rv PATH
Search AVC denials sudo ausearch -m AVC -ts recent

Understand SELinux Contexts

Every labeled object carries a context with four fields:

text
user:role:type:level

For day-to-day targeted-policy administration, focus on type (t field):

Example type Typical use
httpd_t Apache/httpd process domain
httpd_sys_content_t Web content files httpd should read

User and role fields often stay system_u:object_r on system objects. MLS/MCS levels matter on labeled systems but are not the first field to change for a standard web directory fix.


Check SELinux Status and Mode

Read the active mode:

bash
getenforce
output
Enforcing

sestatus adds policy name, configured mode, and mount information:

bash
sestatus
output
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33

Current mode is what the kernel enforces now. Mode from config file is what /etc/selinux/config requests after reboot.


Enforcing vs Permissive vs Disabled

Mode Policy loaded Labels maintained Violations
Enforcing Yes Yes Denied and logged (AVC)
Permissive Yes Yes Logged only—not blocked
Disabled No Can become inconsistent No SELinux enforcement

Enforcing is production default on RHEL: policy blocks disallowed access and writes audit events.

Permissive keeps labels and rules active while you collect AVC data without blocking users. Red Hat recommends permissive over disabling SELinux for troubleshooting.

Disabled turns off the SELinux subsystem. Files created while disabled may lack labels; re-enabling requires careful relabeling. Avoid disabling for ordinary application fixes.


Change Between Enforcing and Permissive Temporarily

setenforce toggles runtime mode until reboot (or until you change it again):

bash
sudo setenforce 0

setenforce prints nothing when the switch succeeds.

Read the runtime mode after switching to permissive:

bash
getenforce
output
Permissive

Return to enforcing when testing finishes:

bash
sudo setenforce 1

Again there is no stdout on success—confirm with getenforce:

bash
getenforce
output
Enforcing

setenforce cannot enable SELinux when the kernel booted with selinux=0—remove that parameter with grubby and follow the relabel workflow below before returning to enforcing mode.


Configure Enforcing or Permissive Mode Persistently

Edit /etc/selinux/config on RHEL-family systems:

bash
grep -E '^SELINUX|^SELINUXTYPE' /etc/selinux/config
output
SELINUX=enforcing
SELINUXTYPE=targeted

Set SELINUX=permissive or SELINUX=enforcing, then reboot for the boot-time mode to match. Runtime setenforce alone does not change this file.


Disable SELinux on RHEL 10

Users search for disable steps, but disabling is not the recommended fix for a mislabeled directory.

On RHEL 10, the supported disable mechanism is a kernel boot parameter, not SELINUX=disabled in /etc/selinux/config (that path was removed as a supported disable method starting with RHEL 9):

bash
sudo grubby --update-kernel ALL --args selinux=0

grubby exits silently when the boot parameter is added. Reboot, then confirm with getenforce (expected: Disabled once the parameter is active). This lab article does not apply that change on the test host.

Implications:

  • New and modified files may be created without consistent labels.
  • Re-enabling requires removing selinux=0, relabeling, and verification—not only editing config files.

To re-enable after a system was booted with selinux=0, remove the disable argument first. The kernel parameter still blocks SELinux from loading even if /etc/selinux/config says enforcing:

bash
sudo grubby --update-kernel ALL --remove-args="selinux=0"

No output appears when removal succeeds—verify later with grubby --info DEFAULT if you need to confirm the kernel command line.

Then follow the relabel sequence in the next section (boot permissive, fixfiles, verify, return to enforcing). For runtime troubleshooting of a single mislabeled directory, use permissive mode and AVC analysis instead. See Disable SELinux for historical context.


Inspect File and Directory Contexts

ls -Z shows DAC mode plus SELinux user, role, type, and level:

bash
ls -Z /etc/hosts /srv/site/index.html
output
system_u:object_r:net_conf_t:s0 /etc/hosts
unconfined_u:object_r:var_t:s0 /srv/site/index.html

On default RHEL policy, new content under /srv/site is var_t. That label is normal until you add a file-context mapping for web content—DAC may look fine while httpd_t still cannot read the file.


Inspect Process Contexts

Running processes carry a domain label:

bash
ps -eZ | grep httpd | head -2
output
system_u:system_r:httpd_t:s0      88280 ?        00:00:00 httpd
system_u:system_r:httpd_t:s0      88281 ?        00:00:00 httpd

Policy allows httpd_t to read certain object types (such as httpd_sys_content_t), not arbitrary types such as var_t on custom content paths.


Understand Default File Context Rules

SELinux remembers expected labels through policy rules, not only what is stored on the inode today.

List policy-defined path mappings:

bash
sudo semanage fcontext -l | grep '/var/www'
output
/var/www(/.*)?                                     all files          system_u:object_r:httpd_sys_content_t:s0 
/var/www(/.*)?/logs(/.*)?                          all files          system_u:object_r:httpd_log_t:s0 
/var/www/[^/]*/cgi-bin(/.*)?                       all files          system_u:object_r:httpd_sys_script_exec_t:s0

Show what policy expects for a path:

bash
matchpathcon /var/www/html
output
/var/www/html	system_u:object_r:httpd_sys_content_t:s0

matchpathcon answers “what label does file-context policy assign to this path?” It reflects configured rules, not which type your application ideally needs.

Before you add a local rule, a custom path under /srv reports the default expectation:

bash
matchpathcon /srv/site/index.html
output
/srv/site/index.html	system_u:object_r:var_t:s0

Red Hat documents similar non-standard web paths (for example under /srv/myweb) as semanage fcontext first, then restorecon—not restorecon alone to httpd_sys_content_t.


Configure a Persistent Custom File Context

When httpd must read content outside /var/www/html, add a persistent mapping, confirm policy expectation, then apply labels.

1. Current inode label:

bash
ls -Z /srv/site/index.html
output
unconfined_u:object_r:var_t:s0 /srv/site/index.html

2. Policy expectation before the custom rule:

bash
matchpathcon /srv/site/index.html
output
/srv/site/index.html	system_u:object_r:var_t:s0

3. Add the persistent path mapping:

bash
sudo semanage fcontext -a -t httpd_sys_content_t '/srv/site(/.*)?'

semanage exits silently when the local rule is stored. The regex (/.*)? covers the directory and descendants.

4. Policy expectation after the rule:

bash
matchpathcon /srv/site/index.html
output
/srv/site/index.html	system_u:object_r:httpd_sys_content_t:s0

5. Apply labels to disk:

bash
sudo restorecon -Rv /srv/site
output
Relabeled /srv/site from unconfined_u:object_r:var_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
Relabeled /srv/site/index.html from unconfined_u:object_r:var_t:s0 to system_u:object_r:httpd_sys_content_t:s0

List local customizations:

bash
sudo semanage fcontext -l -C | grep site
output
/srv/site(/.*)?                                    all files          system_u:object_r:httpd_sys_content_t:s0

Before the semanage step, restorecon on /srv/site/index.html would keep or restore var_t because policy did not yet map that path to httpd_sys_content_t.


Restore the Default Context with restorecon

restorecon applies whatever file-context rules are currently configured—it does not invent httpd_sys_content_t for arbitrary paths.

bash
sudo restorecon -v /srv/site/index.html

When inode and policy already agree, restorecon -v exits with no stdout.

After semanage fcontext defines /srv/site(/.*)?, recursive restore applies the new expectation:

bash
sudo restorecon -Rv /srv/site
output
Relabeled /srv/site/index.html from unconfined_u:object_r:var_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0

Prefer restorecon over guessing a type with chcon—it reads the same rule database semanage fcontext maintains.


Why chcon Changes Can Disappear

Run this demonstration after the /srv/site(/.*)? mapping exists so restorecon has a policy target to restore.

chcon sets the label on the inode immediately. restorecon resets the type to policy unless you use -F to replace the full context.

Demonstration on the lab file:

bash
sudo chcon -t var_t /srv/site/index.html

chcon also prints nothing on success—the label change shows up in ls -Z:

bash
ls -Z /srv/site/index.html
output
system_u:object_r:var_t:s0 /srv/site/index.html

Policy wins on restore:

bash
sudo restorecon -v /srv/site/index.html
output
Relabeled /srv/site/index.html from system_u:object_r:var_t:s0 to system_u:object_r:httpd_sys_content_t:s0

Use chcon for short tests. Persistent custom paths belong in semanage fcontext. Ansible modules can automate the same pattern—see Ansible SELinux file context.


Modify or Remove Local File-Context Rules

Task Command
Modify type on existing rule sudo semanage fcontext -m -t NEWTYPE 'PATH(/.*)?'
Delete local rule sudo semanage fcontext -d 'PATH(/.*)?'
List local rules only sudo semanage fcontext -l -C

After -m or -d, run restorecon on affected paths so inode labels match the updated policy database.


Understand Move vs Copy Context Behaviour

Operation Typical label behaviour
New file created in mapped directory Policy applies at create time (often correct type immediately)
cp into mapped directory New inode—often receives destination policy type
mv into mapped directory Same inode—retains previous label until restorecon

Copy into /srv/site after the mapping existed:

bash
printf 'copytest\n' > /tmp/copydemo.txt

printf creates the source file with no terminal output.

The source file carries the user tmp type:

bash
ls -Z /tmp/copydemo.txt
output
unconfined_u:object_r:user_tmp_t:s0 /tmp/copydemo.txt

Copy into the mapped tree:

bash
sudo cp /tmp/copydemo.txt /srv/site/copydemo.txt

cp succeeds silently—check the new inode label:

bash
ls -Z /srv/site/copydemo.txt
output
unconfined_u:object_r:httpd_sys_content_t:s0 /srv/site/copydemo.txt

The copy received the mapped web content type because cp created a new inode under /srv/site.

Move preserves the source inode label:

bash
printf 'movetest\n' > /tmp/movedemo.txt

Move the inode into /srv/site without creating a new one:

bash
sudo mv /tmp/movedemo.txt /srv/site/movedemo.txt

mv also prints nothing—the inode number stays the same, so the SELinux type is unchanged until relabel:

bash
ls -Z /srv/site/movedemo.txt
output
unconfined_u:object_r:user_tmp_t:s0 /srv/site/movedemo.txt

Run restorecon after bulk moves into service directories.


Find SELinux Denials

Search recent AVC records:

bash
sudo ausearch -m AVC -ts recent

The log can be long—on this lab the denial line for var_t content looked like:

output
type=AVC msg=audit(1786195599.205:5874): avc:  denied  { getattr } for  pid=98046 comm="httpd" path="/srv/site/index.html" dev="dm-0" ino=51913206 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:var_t:s0 tclass=file permissive=0

Read scontext (source domain), tcontext (object type), and tclass. If ausearch shows no AVC while DAC errors appear, suspect Unix permissions or path traversal first.

sealert (from setroubleshoot-server on many systems) can translate AVC into suggestions. Port and boolean fixes are out of scope here—see SELinux ports and booleans when AVC mentions ports or denied { bind } / boolean constraints.


Fix an SELinux File-Context Problem

End-to-end lab on vm1.lab.example. Create the content tree:

bash
sudo mkdir -p /srv/site

Write the page content into that directory:

bash
printf 'lab site\n' | sudo tee /srv/site/index.html

Confirm the inode label under default /srv rules:

bash
ls -Z /srv/site/index.html
output
unconfined_u:object_r:var_t:s0 /srv/site/index.html

Allow Apache filesystem access explicitly—content outside DocumentRoot needs a <Directory> block, not only Alias. Save this in /etc/httpd/conf.d/site-lab.conf:

text
Alias /site/ "/srv/site/"

<Directory "/srv/site">
    Require all granted
</Directory>

Validate the syntax before you reload:

bash
sudo apachectl configtest
output
Syntax OK

Restart httpd so the new alias and directory authorization load:

bash
sudo systemctl restart httpd

systemctl restart exits silently when the unit starts cleanly.

Confirm DAC traverse if parent directories block the apache user (chmod o+x on /srv and /srv/site when needed). Then request the page while the file is still var_t:

bash
curl -s -o /dev/null -w '%{http_code}\n' http://localhost/site/index.html
output
403

Pull the matching AVC line from the audit log:

bash
sudo ausearch -m AVC -ts recent | grep '/srv/site/index.html' | tail -1
output
type=AVC msg=audit(1786195599.205:5874): avc:  denied  { getattr } for  pid=98046 comm="httpd" path="/srv/site/index.html" dev="dm-0" ino=51913206 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:var_t:s0 tclass=file permissive=0

DAC and Apache config were otherwise correct—httpd_t versus var_t is the remaining blocker.

Before any custom mapping, policy still expects var_t for that path:

bash
matchpathcon /srv/site/index.html
output
/srv/site/index.html	system_u:object_r:var_t:s0

Add the persistent mapping and apply labels:

bash
sudo semanage fcontext -a -t httpd_sys_content_t '/srv/site(/.*)?'

Apply labels from the new mapping:

bash
sudo restorecon -Rv /srv/site
output
Relabeled /srv/site from unconfined_u:object_r:var_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
Relabeled /srv/site/index.html from unconfined_u:object_r:var_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0

Retry the HTTP request while enforcing mode stays enabled:

bash
curl -s http://localhost/site/index.html
output
lab site

SELinux stayed enforcing throughout—the fix was labeling, not disabling MAC or widening Apache beyond Require all granted.


Relabel Filesystems When Re-enabling SELinux

After SELinux was disabled with selinux=0 or files were restored without labels, inode contexts may be wrong system-wide. Targeted restorecon -Rv /path fixes one tree; full recovery needs a scheduled relabel.

RHEL 10 documentation requires booting permissive for that relabel boot so unlabeled files needed early in startup do not break the boot path. Example recovery after removing selinux=0:

bash
sudo grubby --update-kernel ALL --remove-args="selinux=0"

grubby prints nothing when the argument is removed successfully.

Add a temporary permissive boot argument for the relabel cycle:

bash
sudo grubby --update-kernel ALL --args="enforcing=0"

grubby exits silently when enforcing=0 is added.

Schedule a full relabel on the next boot:

bash
sudo fixfiles -F onboot
output
System will relabel on next boot

Reboot into permissive mode so early startup can access files before relabel completes:

bash
sudo reboot

A reboot ends the SSH session—log in again after the system comes back.

After relabel completes and you review AVC output for unexpected denials, remove the temporary permissive boot argument:

bash
sudo grubby --update-kernel ALL --remove-args="enforcing=0"

Reboot again to return to enforcing mode:

bash
sudo reboot

Confirm with getenforce after the second reboot—expected: Enforcing when enforcing=0 is no longer on the kernel command line.

Do not use full-system fixfiles as the first response to one mislabeled application directory—fix the path with semanage fcontext and restorecon first.


Common SELinux Mistakes

Mistake Why it fails
Disabling SELinux to “make it work” Removes MAC protection; relabel pain on re-enable
chmod 777 on blocked paths DAC wide open but SELinux may still deny
chcon without semanage fcontext restorecon or relabel reverts the change
Changing context without reading AVC Wrong type—ports or booleans may be the real blocker
Correct file type but service still fails Check SELinux ports and booleans
Ignoring Unix permissions DAC denial happens before SELinux
Global fixfiles for one bad directory Slow; use path-specific restorecon first

References


Summary

SELinux enforces type-aware access after normal permission checks. The practical workflow is inspect mode with getenforce and sestatus, read labels with ls -Z, compare expectations with matchpathcon, search ausearch -m AVC, then fix persistent mappings with semanage fcontext and restorecon—not chmod 777 and not disabling MAC for routine app issues.

On this lab host, httpd in enforcing mode returned 403 for /srv/site even with Require all granted and DAC traverse fixed—AVC showed httpd_t versus var_t. After semanage fcontext and restorecon, matchpathcon expected httpd_sys_content_t and HTTP returned 200. Before the mapping, matchpathcon and restorecon both treated the path as var_t.

Use permissive mode temporarily when you need denial logs without blocking traffic. Reserve disabled SELinux and fixfiles full relabel for recovery scenarios. Port labels and booleans are the next layer when file contexts are already correct—see SELinux ports and booleans.


Frequently Asked Questions

1. What is the difference between enforcing and permissive SELinux?

Enforcing blocks operations that violate policy and logs AVC denials. Permissive keeps policy loaded and labels in place but logs denials without blocking. Permissive is the recommended troubleshooting mode on RHEL instead of disabling SELinux entirely.

2. Why does chmod 644 not fix an SELinux permission problem?

SELinux evaluates Mandatory Access Control after traditional Unix permissions. A path can be world-readable in DAC yet still blocked when the process domain and object type do not match policy. Inspect labels with ls -Z and AVC messages before you widen chmod.

3. Should I use chcon or semanage fcontext for a custom web directory?

Use semanage fcontext to add a persistent policy mapping, then restorecon to apply it. chcon changes the inode label immediately but restorecon or relabeling can revert it to the policy-defined type.

4. Why does restorecon change my chcon label?

restorecon resets labels to match file_context policy rules from semanage fcontext and the loaded policy. chcon is useful for short tests; persistent configuration belongs in semanage fcontext.

5. When should I relabel the whole system with fixfiles?

Use targeted restorecon on the mislabeled directory first. Full-system relabel with fixfiles is for recovery after disabling SELinux, major policy changes, or restoring from backups without labels—not as the first fix for one wrong directory.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)