Mandatory Access Control

SELinux

Label-based mandatory access control: contexts, policies, and living with denials.

SELinux is the most comprehensive mandatory access control system in mainstream Linux, and it is the default on the Red Hat family (RHEL, Fedora, CentOS Stream, and their derivatives). Where AppArmor (AppArmor page) reasons about filesystem paths, SELinux reasons about labels: every process and every object carries a security context, and policy is a set of rules allowing one type to act on another. This label-based design closes gaps that path-based rules cannot (a hard link or a bind mount cannot smuggle a file past its label), at the cost of a steeper learning curve. A note for this lab: Ubuntu 24.04 does not ship SELinux by default (it uses AppArmor), so treat the commands here as reference for Red Hat family systems. You can install the tooling on Ubuntu for study, but the natural place to practise SELinux is a Fedora or RHEL VM.

Labels, types, and how decisions are made

Every context has the form user:role:type:level, for example system_u:object_r:httpd_sys_content_t:s0. For everyday policy the type field is what matters; this is “type enforcement,” the heart of SELinux.

Context fieldMeaningExample
userSELinux user (not the Unix user)system_u, unconfined_u
roleRole, mainly for user domainsobject_r, system_r
typeThe type / domain used for enforcementhttpd_t, httpd_sys_content_t
levelMLS/MCS sensitivity and categoriess0, s0:c1,c2

A process runs in a domain (a type, e.g. httpd_t). A file has a type (e.g. httpd_sys_content_t). Policy contains rules like allow httpd_t httpd_sys_content_t:file { read open getattr };. A decision is simple: if a rule allows the process’s domain to perform the action on the object’s type, it is permitted; otherwise it is denied. This is why SELinux stops a compromised web server dead even as root, the httpd_t domain has no rule permitting it to read shadow_t, and being UID 0 does not add one.

# See the labels on processes and files (Red Hat family)
ps -eZ | grep httpd | head -1
ls -Z /var/www/html/index.html

Expected output:

system_u:system_r:httpd_t:s0    1123 ?        00:00:00 httpd
unconfined_u:object_r:httpd_sys_content_t:s0 /var/www/html/index.html

The -Z flag on the coreutils tools shows contexts everywhere: ps -eZ, ls -Z, id -Z, netstat -Z.

Enforcing, permissive, and reading denials

SELinux runs in one of three states. The distinction between enforcing and permissive is your main diagnostic lever.

ModeBehaviour
enforcingPolicy is applied; violations are blocked and logged
permissiveViolations are logged but allowed; nothing is blocked
disabledSELinux is off entirely (avoid; requires a reboot to re-enable properly)
# Current mode and full status
getenforce
sestatus

Expected output:

Enforcing
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
Current mode:                   enforcing
Loaded policy name:             targeted

When enforcing SELinux blocks an action, it writes an AVC (Access Vector Cache) denial to the audit log. That record is the equivalent of AppArmor’s DENIED line and contains everything needed to fix the issue.

# Human-readable summary of recent denials
sudo ausearch -m avc -ts recent | tail -8
# Even friendlier: analysis plus suggested fix
sudo sealert -a /var/log/audit/audit.log | head -20

Expected output:

type=AVC msg=audit(1720080000.123:456): avc:  denied  { read } for  pid=1123 comm="httpd" name="report.html" dev="dm-0" ino=98231 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

Read it left to right: the httpd_t domain was denied read on a file labelled user_home_t. The problem is obvious once you see it, the file has the wrong label (a home-directory type instead of a web-content type), so httpd is correctly forbidden from serving it.

Fixing denials properly instead of setenforce 0

The reflex to run setenforce 0 is the cardinal SELinux sin: it disables the entire policy to work around one mislabelled file, removing protection from every domain on the box. Almost every denial has one of three proper fixes.

  1. Wrong label (most common). Relabel the object to the type policy expects. Use restorecon to reset to the default for that path, or semanage fcontext to define a new default first.
# Give the file the correct web-content label
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/html(/.*)?"
sudo restorecon -Rv /var/www/html

Expected output:

Relabeled /var/www/html/report.html from unconfined_u:object_r:user_home_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
  1. A legitimate behaviour toggled by a boolean. Much optional behaviour (httpd making network connections, using NFS home dirs) is gated by tunable booleans rather than requiring custom policy.
sudo setsebool -P httpd_can_network_connect on
getsebool httpd_can_network_connect

Expected output:

httpd_can_network_connect --> on
  1. A genuinely new access no policy covers. Generate a targeted local policy module from the denial with audit2allow, review the rule it proposes, and load only that. This grants exactly the one access rather than disabling everything.

If none of these fit, the denial may be catching an actual misconfiguration or attack, which is SELinux doing its job. Use permissive mode as a temporary diagnostic (setenforce 0 briefly, or a per-domain permissive with semanage permissive -a httpd_t) to collect the full set of denials without breaking the app, then fix and return to enforcing.

Practical Guidance

  1. Never leave SELinux disabled or globally permissive as a “fix.” Relabel, set a boolean, or add a targeted audit2allow module instead; each addresses one denial without dropping the whole policy.
  2. Suspect labels first. The overwhelming majority of real-world denials are a file or port with the wrong type; ls -Z and restorecon resolve most of them.
  3. Learn ausearch, sealert, and audit2why before you touch policy. They translate raw AVC records into plain explanations and often name the exact fix.
  4. Use per-domain permissive mode (semanage permissive -a <type>) for diagnostics, not the global setenforce 0. It keeps every other domain protected while you investigate one.
  5. Practise on a Red Hat family or Fedora VM, since Ubuntu ships AppArmor by default. The concepts map to the AppArmor page, but the tooling and defaults differ, and SELinux is where you will meet labels in the wild, including Kubernetes SELinux options in a pod securityContext.