| Tested on | RHEL 10.2 (Coughlan) — vm1.lab.example |
|---|---|
| Package | policycoreutils 3.10-1.el10policycoreutils-python-utils 3.10-1.el10selinux-policy-targeted 42.1.18-4.el10_2.1httpd 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 |
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:
user:role:type:levelFor 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:
getenforceEnforcingsestatus adds policy name, configured mode, and mount information:
sestatusSELinux 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: 33Current 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):
sudo setenforce 0setenforce prints nothing when the switch succeeds.
Read the runtime mode after switching to permissive:
getenforcePermissiveReturn to enforcing when testing finishes:
sudo setenforce 1Again there is no stdout on success—confirm with getenforce:
getenforceEnforcingsetenforce 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:
grep -E '^SELINUX|^SELINUXTYPE' /etc/selinux/configSELINUX=enforcing
SELINUXTYPE=targetedSet 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):
sudo grubby --update-kernel ALL --args selinux=0grubby 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:
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:
ls -Z /etc/hosts /srv/site/index.htmlsystem_u:object_r:net_conf_t:s0 /etc/hosts
unconfined_u:object_r:var_t:s0 /srv/site/index.htmlOn 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:
ps -eZ | grep httpd | head -2system_u:system_r:httpd_t:s0 88280 ? 00:00:00 httpd
system_u:system_r:httpd_t:s0 88281 ? 00:00:00 httpdPolicy 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:
sudo semanage fcontext -l | grep '/var/www'/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:s0Show what policy expects for a path:
matchpathcon /var/www/html/var/www/html system_u:object_r:httpd_sys_content_t:s0matchpathcon 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:
matchpathcon /srv/site/index.html/srv/site/index.html system_u:object_r:var_t:s0Red 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:
ls -Z /srv/site/index.htmlunconfined_u:object_r:var_t:s0 /srv/site/index.html2. Policy expectation before the custom rule:
matchpathcon /srv/site/index.html/srv/site/index.html system_u:object_r:var_t:s03. Add the persistent path mapping:
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:
matchpathcon /srv/site/index.html/srv/site/index.html system_u:object_r:httpd_sys_content_t:s05. Apply labels to disk:
sudo restorecon -Rv /srv/siteRelabeled /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:s0List local customizations:
sudo semanage fcontext -l -C | grep site/srv/site(/.*)? all files system_u:object_r:httpd_sys_content_t:s0Before 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.
sudo restorecon -v /srv/site/index.htmlWhen inode and policy already agree, restorecon -v exits with no stdout.
After semanage fcontext defines /srv/site(/.*)?, recursive restore applies the new expectation:
sudo restorecon -Rv /srv/siteRelabeled /srv/site/index.html from unconfined_u:object_r:var_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0Prefer 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:
sudo chcon -t var_t /srv/site/index.htmlchcon also prints nothing on success—the label change shows up in ls -Z:
ls -Z /srv/site/index.htmlsystem_u:object_r:var_t:s0 /srv/site/index.htmlPolicy wins on restore:
sudo restorecon -v /srv/site/index.htmlRelabeled /srv/site/index.html from system_u:object_r:var_t:s0 to system_u:object_r:httpd_sys_content_t:s0Use 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:
printf 'copytest\n' > /tmp/copydemo.txtprintf creates the source file with no terminal output.
The source file carries the user tmp type:
ls -Z /tmp/copydemo.txtunconfined_u:object_r:user_tmp_t:s0 /tmp/copydemo.txtCopy into the mapped tree:
sudo cp /tmp/copydemo.txt /srv/site/copydemo.txtcp succeeds silently—check the new inode label:
ls -Z /srv/site/copydemo.txtunconfined_u:object_r:httpd_sys_content_t:s0 /srv/site/copydemo.txtThe copy received the mapped web content type because cp created a new inode under /srv/site.
Move preserves the source inode label:
printf 'movetest\n' > /tmp/movedemo.txtMove the inode into /srv/site without creating a new one:
sudo mv /tmp/movedemo.txt /srv/site/movedemo.txtmv also prints nothing—the inode number stays the same, so the SELinux type is unchanged until relabel:
ls -Z /srv/site/movedemo.txtunconfined_u:object_r:user_tmp_t:s0 /srv/site/movedemo.txtRun restorecon after bulk moves into service directories.
Find SELinux Denials
Search recent AVC records:
sudo ausearch -m AVC -ts recentThe log can be long—on this lab the denial line for var_t content looked like:
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=0Read 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:
sudo mkdir -p /srv/siteWrite the page content into that directory:
printf 'lab site\n' | sudo tee /srv/site/index.htmlConfirm the inode label under default /srv rules:
ls -Z /srv/site/index.htmlunconfined_u:object_r:var_t:s0 /srv/site/index.htmlAllow Apache filesystem access explicitly—content outside DocumentRoot needs a <Directory> block, not only Alias. Save this in /etc/httpd/conf.d/site-lab.conf:
Alias /site/ "/srv/site/"
<Directory "/srv/site">
Require all granted
</Directory>Validate the syntax before you reload:
sudo apachectl configtestSyntax OKRestart httpd so the new alias and directory authorization load:
sudo systemctl restart httpdsystemctl 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:
curl -s -o /dev/null -w '%{http_code}\n' http://localhost/site/index.html403Pull the matching AVC line from the audit log:
sudo ausearch -m AVC -ts recent | grep '/srv/site/index.html' | tail -1type=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=0DAC 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:
matchpathcon /srv/site/index.html/srv/site/index.html system_u:object_r:var_t:s0Add the persistent mapping and apply labels:
sudo semanage fcontext -a -t httpd_sys_content_t '/srv/site(/.*)?'Apply labels from the new mapping:
sudo restorecon -Rv /srv/siteRelabeled /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:s0Retry the HTTP request while enforcing mode stays enabled:
curl -s http://localhost/site/index.htmllab siteSELinux 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:
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:
sudo grubby --update-kernel ALL --args="enforcing=0"grubby exits silently when enforcing=0 is added.
Schedule a full relabel on the next boot:
sudo fixfiles -F onbootSystem will relabel on next bootReboot into permissive mode so early startup can access files before relabel completes:
sudo rebootA 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:
sudo grubby --update-kernel ALL --remove-args="enforcing=0"Reboot again to return to enforcing mode:
sudo rebootConfirm 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
- getenforce(8) — Linux manual page
- restorecon(8) — Linux manual page
- semanage-fcontext(8) — Linux manual page
- Red Hat Enterprise Linux 10 — Using SELinux
- Red Hat Enterprise Linux 10 — Non-standard directories and semanage fcontext
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.

