SELinux vs. AppArmor: Mandatory Access Control on Linux
How SELinux's label-based policy and AppArmor's path-based profiles both extend Linux's discretionary permission model, and how to work with each day to day.
Standard Unix permissions are discretionary access control (DAC): the owner of a file decides who can read, write, or execute it, and root can override any of it. Mandatory access control (MAC) adds a second, independent layer of restriction that even root can’t simply override by fiat — a system-wide policy, defined separately from file ownership, that constrains what a process is allowed to do regardless of the discretionary permissions it would otherwise have. SELinux and AppArmor are Linux’s two major, actively maintained implementations of that idea, and they take almost opposite approaches to expressing policy.
SELinux: labels on everything
SELinux (originally developed by the NSA, now a mainline kernel feature via the Linux Security Module framework) works by attaching a security context — a label like user_u:role_r:httpd_t:s0 — to every process and every object (files, sockets, even system properties on Android). Policy rules are written in terms of these labels, not paths: “a process running in the httpd_t domain may read files labeled httpd_sys_content_t.”
ls -Z /var/www/html/index.html
# system_u:object_r:httpd_sys_content_t:s0 index.html
ps -eZ | grep httpd
# system_u:system_r:httpd_t:s0 1234 ? httpd
Because the policy references labels rather than literal paths, moving or renaming a file doesn’t automatically change what it’s allowed to do — but it also means a file that isn’t labeled correctly (copied in from elsewhere, restored from a backup with the wrong context) can be denied access even though its ordinary Unix permissions look completely fine, which is the single most common source of “permission denied but the permissions look right” confusion for SELinux newcomers.
restorecon -Rv /var/www/html/ # reset paths to their policy-defined default labels
semanage fcontext -a -t httpd_sys_content_t "/srv/web(/.*)?"
Reading SELinux denials
Every denial is logged, and audit2why translates the cryptic AVC (Access Vector Cache) denial into a human-readable explanation, while audit2allow can generate a policy module to permit exactly what was denied — useful for understanding a problem, though generating overly broad allow-rules from it uncritically defeats much of the point:
ausearch -m avc -ts recent | audit2why
sealert -a /var/log/audit/audit.log
getenforce # Enforcing, Permissive, or Disabled
setenforce 0 # Permissive: log denials but don't block them — a debugging aid, not a fix
AppArmor: profiles tied to paths
AppArmor, the alternative found by default on Debian, Ubuntu, and SUSE, takes a deliberately simpler approach: policy (a profile) is written per-executable and expressed directly in terms of file paths and capabilities, rather than an abstract label space.
# /etc/apparmor.d/usr.sbin.nginx
/usr/sbin/nginx {
#include <abstractions/base>
/etc/nginx/** r,
/var/log/nginx/*.log w,
/var/www/html/** r,
network inet stream,
}
Because profiles reference concrete paths, they’re generally easier to read and write for a specific application than an equivalent SELinux policy — the trade-off being that path-based rules can be less precise about why access is being granted, and a bind mount or symlink pointing outside the profiled paths can create gaps a label-based system wouldn’t have.
aa-status # profiles loaded, and their mode
aa-complain /usr/sbin/nginx # complain mode: log violations, don't enforce
aa-enforce /usr/sbin/nginx # enforce mode: actually block violations
Generating and refining a profile
AppArmor’s tooling leans on a practical workflow: put a profile in complain mode, exercise the application through its normal operations, then let aa-logprof review the logged violations and interactively decide which should become permanent allow-rules:
aa-genprof /usr/local/bin/myapp
# ... exercise the application ...
aa-logprof
Choosing between them
In practice this usually isn’t a choice you make from scratch — it’s determined by distribution: Red Hat Enterprise Linux, Fedora, and CentOS ship SELinux by default and expect it enabled; Ubuntu, Debian, and SUSE ship AppArmor. Both accomplish the same underlying goal — confining what a compromised process can do beyond its Unix permissions — and both are frequently disabled by frustrated administrators rather than debugged properly, which defeats the point of either. The pragmatic rule for both: use permissive/complain mode plus the respective log-analysis tooling (audit2why/sealert for SELinux, aa-logprof for AppArmor) to build or fix a policy, rather than reaching for enforcement’s off switch the moment something is denied.