SELinux and AppArmor
SELinux and AppArmor are both Linux Security Modules (LSM) — kernel hooks that add a mandatory access control (MAC) layer on top of the ordinary owner/group/rwx permissions every Linux process already obeys. The standard permissions are discretionary: the file's owner decides who gets access, and a process running as root bypasses them entirely. A MAC layer adds a second, non-negotiable verdict written by the administrator into kernel-enforced policy, and that verdict can deny an action even when the file permissions would allow it and even when the process is root.
The operational point is containment. When a network-facing daemon is compromised, MAC is what stops the attacker pivoting from "I own the web server process" to "I read /etc/shadow and wrote a cron job." Ubuntu and Debian ship AppArmor enabled by default; RHEL, Fedora, and their derivatives ship SELinux. They solve the same problem with very different models — AppArmor confines a binary by the file paths it may touch, SELinux labels every process and object and checks the labels — and that difference dictates how you debug a denial.
Discretionary versus Mandatory Access Control
Classic Unix permissions are discretionary access control: the owner sets the bits, the owner can hand out access, and UID 0 is exempt. That model is fine for keeping users out of each other's files, but it offers nothing once a privileged process is hijacked. An attacker who lands code execution inside a root-owned daemon inherits every capability that daemon had — which, with DAC alone, is the whole machine.
MAC inverts the trust. Policy is written once by the administrator and the kernel enforces it on every relevant syscall, regardless of UID. A confined Nginx process may be permitted to read /var/www and bind port 443 and nothing else; if exploited, an attempt to read /etc/shadow or open an outbound socket to a C2 host is denied by the kernel, logged, and the breach is boxed into the daemon's intended job. The trade-off is that policy must describe the legitimate behavior accurately — too tight and the service breaks, too loose and the confinement is theater.
AppArmor
AppArmor is path-based. Each confined program gets a profile in /etc/apparmor.d/ named after the binary's full path (for example usr.sbin.nginx), listing the files it may read, write, or execute and the capabilities it may use. A profile runs in one of two modes: enforce, where violations are blocked and logged, or complain, where violations are allowed but logged so you can see what a real workload needs before you tighten it.
Day-to-day work is a handful of commands. aa-status shows which profiles are loaded and in which mode; aa-complain and aa-enforce flip a single profile between modes; aa-genprof generates a starter profile by watching a binary run, and aa-logprof walks the recent denials and proposes profile edits interactively.
# list loaded profiles and their modes sudo aa-status # put one profile into complain mode to learn what it needs sudo aa-complain /etc/apparmor.d/usr.sbin.nginx # after running real traffic, review denials and update the profile sudo aa-logprof # tighten back to enforce sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
Because profiles key on paths, AppArmor is quick to read and quick to reason about — a profile is essentially an allow-list of files. The cost of that simplicity is that it follows paths, not data: a file reachable through two different paths (a bind mount, a hard link, a symlink the policy didn't anticipate) can slip the rules, which is exactly the gap SELinux's labeling closes.
SELinux
SELinux is label-based. Every process and every object — files, sockets, ports, directories — carries a security context of the form user:role:type:level, and the core mechanism, type enforcement, decides access by checking the process's type against the object's type in a compiled policy. The label travels with the object on disk (stored in the security.selinux extended attribute), so renaming or hard-linking a file does not change what may touch it. See contexts with ls -Z for files and ps -Z for processes.
SELinux has three system states: enforcing (policy is applied and violations blocked), permissive (violations are logged but allowed — the learning and debugging mode), and disabled (the LSM is off entirely). getenforce prints the current state and setenforce 1/setenforce 0 toggles between enforcing and permissive at runtime; the boot default lives in /etc/selinux/config.
# current mode and file/process contexts getenforce ls -Z /var/www/html ps -Z # move to permissive to debug, then back to enforcing sudo setenforce 0 sudo setenforce 1 # reapply the correct labels after moving files into place sudo restorecon -Rv /var/www/html
Most tuning never touches custom policy. semanage fcontext registers the correct type for a path so future relabels stick; restorecon resets a file's label to what policy expects; and SELinux booleans — toggled with setsebool -P — switch whole behaviors on or off, such as httpd_can_network_connect to let a web server make outbound connections. When no boolean or fcontext fits, audit2allow reads the denial records and generates a custom policy module, but that is the last resort, not the first.
Troubleshooting Denials
A MAC denial looks like a permission error that chmod and chown cannot fix, because the file permissions were never the problem. Both systems log every denial, and reading those logs is the entire skill. AppArmor writes apparmor="DENIED" lines to the kernel log (dmesg) and, where auditd is running, to /var/log/audit/audit.log. SELinux writes AVC (access vector cache) denial records to /var/log/audit/audit.log; ausearch -m avc -ts recent and sealert turn them into readable diagnoses with suggested fixes.
# AppArmor denials sudo dmesg | grep -i apparmor # SELinux AVC denials, with human-readable advice sudo ausearch -m avc -ts recent sudo sealert -a /var/log/audit/audit.log
The fix is almost never to switch the whole system off. The correct loop is: reproduce the denial, read the log, and address the specific access — relabel with restorecon, flip a boolean, add a path to an AppArmor profile via aa-logprof, or generate a narrow module with audit2allow. Disabling MAC to "unblock" a single service throws away the protection for every other service on the box, and on SELinux a system that booted disabled then re-enabled will have unlabeled files that need a full filesystem relabel (touch /.autorelabel; reboot) before it works correctly.
AppArmor — path-based profiles, one per binary, in /etc/apparmor.d/. Easy to read and edit because a profile is an allow-list of file paths, with enforce and complain modes. The default on Debian and Ubuntu. Choose it when you want fast, per-application confinement that an operator can audit by eye.
SELinux — label-based type enforcement, with a context on every process and object that travels with the file. Far more granular and harder to bypass than path rules, at the cost of a steeper model and a relabeling discipline. The default on RHEL, Fedora, and CentOS Stream. Choose it (or accept it) when policy must follow data across paths and links, or when your compliance baseline mandates it.
In practice you do not pick: you run whatever the distribution ships. The mistake is forcing the wrong one onto a distro — bolting SELinux onto Ubuntu or stripping it out of RHEL — instead of learning the one that is already integrated and supported there.
- Reaching for
setenforce 0or setting/etc/selinux/configtodisabledas the fix for one service — it removes confinement from every daemon on the host, and a full disabled boot leaves files unlabeled so re-enabling requires an autorelabel reboot. On RHEL 9 the config-filedisabledsetting is deprecated and no longer fully unloads the LSM; the kernel boots SELinux enabled and only theselinux=0kernel parameter disables it outright. - Copying or moving files into
/var/wwwor/srvand then hitting denials because the files kept their source labels — the content is fine, the SELinux type is wrong, and the fix isrestorecon -Rv, notchmod. - Deleting an AppArmor profile from
/etc/apparmor.d/to stop the denials instead of setting it to complain mode — you lose the confinement entirely rather than learning what the binary actually needs. - Running
chmod 777against a MAC denial — the DAC bits were never the blocker, so it widens the discretionary permissions for no benefit while the kernel keeps denying on the label or path. - Debugging SELinux without the policy and troubleshooting tools installed — without
policycoreutils-python-utils,setroubleshoot-server, and the SELinux man pages, you have AVC lines and nosealert,audit2allow, orsemanageto interpret or fix them. - Treating an AppArmor profile as portable across distributions or versions — path rules assume a specific binary location and filesystem layout, and a profile written for one Ubuntu release can silently misbehave on another.
- Keep MAC enabled. When something breaks, drop to permissive (SELinux) or complain mode (the affected AppArmor profile) to learn the access pattern — never to
disabled, which protects nothing. - Build policy from real denials: run
aa-logprofon AppArmor andaudit2allowon SELinux against logs from actual traffic, instead of hand-writing rules you guessed at. - Run
restorecon -Rvon any path after copying, moving, or restoring files into it on a SELinux system, and register non-default locations withsemanage fcontextso the labels survive future relabels. - Prefer a boolean over a custom module — check
getsebool -aand toggle withsetsebool -P(for examplehttpd_can_network_connect on) before writing any policy of your own. - Bring up a new AppArmor profile in complain mode, exercise the service under representative load, then promote it to enforce with
aa-enforceonce the denials stop appearing. - Install the troubleshooting tooling up front —
setroubleshoot-serverpluspolicycoreutils-python-utilson RHEL, theapparmor-utilspackage on Debian/Ubuntu — so you havesealert,audit2allow, andaa-logprofready before the first incident.
Knowledge Check
What does mandatory access control add that ordinary rwx permissions cannot provide?
- A kernel-enforced policy that can deny an action even to a root-owned process whose file permissions would otherwise allow it
- Transparent on-disk encryption of file contents that stay unreadable to any process unless the active policy explicitly unlocks the bytes
- A finer-grained set of per-entry owner and group bits so that non-root users can delegate file access to other accounts more precisely
- Automatic revocation of a file's permissions by the kernel once a fixed, preconfigured time window has fully elapsed
A file works when served from /var/www/html but fails after you cp it in from a home directory, with a denial that chmod 777 does not fix. On a SELinux system, what is the cause?
- The copied file kept its source SELinux type label, which the web server's type is not allowed to access — relabel it with
restorecon - The underlying DAC permissions are still far too narrow for the web server and need a
chmodsetting even wider than 777 to finally grant access - SELinux has cached the file's old path in its access vector cache and that stale entry must be flushed out by running
setenforce 0 - The file's owner and group were silently changed to your account during the copy and must be reset back to the web user with
chown
How do AppArmor and SELinux differ in how they decide what a confined process may access?
- AppArmor matches on file paths in a per-binary profile; SELinux matches on security labels carried by every process and object
- AppArmor uses security labels stored in each file's extended attributes; SELinux instead uses path-based allow-lists defined per binary
- AppArmor enforces its profiles only against root-owned processes; SELinux enforces its policy only against unprivileged non-root users
- AppArmor evaluates access statically at compile time; SELinux evaluates it only once at system boot
One daemon's MAC denial is breaking a deploy. Why is setenforce 0 (or disabling the profile) the wrong fix?
- It removes confinement from every service on the host, not just the one you are debugging — permissive mode or a targeted policy change addresses only the failing access
- It permanently corrupts the compiled binary policy database stored on disk and then requires a full from-scratch reinstall of the entire distribution to recover the host
- It immediately blocks all inbound and outbound network traffic on every interface of the host until the system is fully rebooted back into enforcing mode
- It silently re-enables strict enforcement again all on its own after only a few minutes, so the very same denial inevitably returns in the middle of the deploy
You got correct