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 field | Meaning | Example |
|---|---|---|
| user | SELinux user (not the Unix user) | system_u, unconfined_u |
| role | Role, mainly for user domains | object_r, system_r |
| type | The type / domain used for enforcement | httpd_t, httpd_sys_content_t |
| level | MLS/MCS sensitivity and categories | s0, 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.
| Mode | Behaviour |
|---|---|
| enforcing | Policy is applied; violations are blocked and logged |
| permissive | Violations are logged but allowed; nothing is blocked |
| disabled | SELinux 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.
- Wrong label (most common). Relabel the object to the type policy expects. Use
restoreconto reset to the default for that path, orsemanage fcontextto 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
- 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
- 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
- Never leave SELinux disabled or globally permissive as a “fix.” Relabel, set a boolean, or add a targeted
audit2allowmodule instead; each addresses one denial without dropping the whole policy. - Suspect labels first. The overwhelming majority of real-world denials are a file or port with the wrong type;
ls -Zandrestoreconresolve most of them. - Learn
ausearch,sealert, andaudit2whybefore you touch policy. They translate raw AVC records into plain explanations and often name the exact fix. - Use per-domain permissive mode (
semanage permissive -a <type>) for diagnostics, not the globalsetenforce 0. It keeps every other domain protected while you investigate one. - 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.