SELinux vs AppArmor

SELinux and AppArmor are the two dominant mandatory access control (MAC) modules in production Linux, and they embody opposite philosophies of how to name the things you are protecting. SELinux is label-based type enforcement: every process and every object carries a security label (a type) stored in the inode, and the policy is a comprehensive, system-wide allow-list over those labels — because the label is bound to the inode, it follows the object through renames, hardlinks, and bind-mounts, but authoring and auditing that policy is steep. AppArmor is path-based per-program confinement: a profile is attached to an executable’s filesystem path, and inside it you list the file paths, capabilities, and network operations that program may use — profiles are short, human-readable text and easy to write, but because rules name paths rather than labels they have rename/hardlink/bind-mount subtleties, and only programs that have a profile are confined at all. Both are Linux Security Modules, both enforce MAC the discretionary UNIX permissions cannot, and they ship as the distribution default on opposite camps: Red Hat / Fedora / CentOS / Rocky / Alma = SELinux; Ubuntu / Debian / SUSE = AppArmor. This note is the honest, balanced comparison.

This note pins to the 6.12 / 6.18 long-term-support kernels (the 2026 LTS lines). Both modules are in-tree LSMs in those kernels; the kernel’s own SELinux and AppArmor admin-guide documents, the AppArmor apparmor.d(5)/apparmor(7) man pages, the SELinux Notebook, and AppArmor’s own comparison page are the primary sources. Distribution defaults are dated to the 2026 distro generation.


Mental Model

The single axis that explains almost every difference is: what does a rule name? SELinux rules name labels (types) that are attached to objects; AppArmor rules name paths (and the binary a profile is attached to). Everything downstream — completeness, ease of authoring, the rename/hardlink edge cases, the unconfined-by-default behavior — flows from that one choice.

flowchart TB
  subgraph SE["SELinux — label-based"]
    SEP["Process<br/>label httpd_t"]
    SEF["File<br/>xattr security.selinux<br/>= httpd_sys_content_t"]
    SEP -->|"allow httpd_t httpd_sys_content_t:file read"| SEF
    SENOTE["Label lives in the inode →<br/>survives rename / hardlink / bind-mount"]
  end
  subgraph AA["AppArmor — path-based"]
    AAP["Process running<br/>/usr/sbin/nginx<br/>(profile attached by PATH)"]
    AAF["File at path<br/>/var/www/**"]
    AAP -->|"/var/www/** r,"| AAF
    AANOTE["Rule names the PATH →<br/>a different path to the same inode<br/>(hardlink / bind-mount) is a different rule"]
  end

The two MAC models side by side. What it shows: SELinux mediates an access by comparing the label on the process against the label on the object (read from the inode’s security.selinux xattr); AppArmor mediates by matching the path the process used against the path globs in the profile attached to that binary. The insight to take: because SELinux binds identity to the inode, two names for the same file (a hardlink, a bind-mount) share one label and one rule — whereas AppArmor reasons about the path string, so the same inode reached by a different path is, to the policy, a different thing. That is the root of both SELinux’s “labels follow the object” strength and AppArmor’s path-aliasing care-points.


How Each Model Works

SELinux: labels, type enforcement, system-wide

In SELinux every subject and object has a security context user:role:type[:level], and the decisive field is the type. The kernel stores a file’s label in the security.selinux extended attribute (xattr) on the inode — per the SELinux Notebook, it can be viewed with ls -Z or getfattr and looks like security.selinux="unconfined_u:object_r:user_home_t:s0". Because the label is part of the inode’s metadata, moving a file retains its label while copying it gives it the destination directory’s label — the label is bound to the object, not its name. The policy is a vast allow-list of allow source_type target_type : class permission; rules; anything not explicitly allowed is denied. This is type enforcement (TE), and its mechanics live in SELinux Type Enforcement and Labels; the policy language, the targeted policy, and booleans live in SELinux Policy and Booleans. The defining property for this comparison: SELinux aspires to a clean, system-wide model where every resource (process, file, socket, IPC object) has a label, and the policy reasons uniformly over all of them.

AppArmor: profiles, paths, per-program

AppArmor takes “a task centered policy, with task ‘profiles’ being created and loaded from user space” (per the kernel AppArmor doc). A profile is named by, and attached to, an executable’s absolute path: per apparmor.d(5), when “the name is an applications full absolute path name eg. /usr/bin/firefox … the name is also used as the profile’s exec attachment conditional.” Inside the profile you list rules — file paths with permission modes, capabilities, and network operations:

/usr/sbin/nginx {
  capability net_bind_service,
  network inet tcp,
  /etc/nginx/** r,
  /var/www/** r,
  /var/log/nginx/*.log w,
}

File rules combine a path glob with a mode (r read, w write, a append, l link, k lock, and exec-transition modes like ix/px/cx); the wildcards are AppArmor’s: * matches within a directory level, ** matches recursively across /. The profile is loaded by apparmor_parser (via apparmor.service) and, per apparmor(7), “applied to a process at exec time … once a profile is loaded for a program, that program will be confined on the next exec.” The defining property: AppArmor confines from “the perspective of the application,” one program at a time.


The Decisive Differences

Label-follows-object vs path-names-object

This is the substantive security difference, not a stylistic one. Because a SELinux label is stored in the inode, a hardlink to a file shares the same inode and therefore the same label — there is exactly one label no matter how many names point at it, and a bind-mount of the file is still the same inode with the same label. AppArmor rules name paths, so a hardlink that gives a sensitive file a second name under a directory the profile does allow, or a bind-mount that surfaces a file at a path the profile permits, is — to a naive profile — a different, permitted path. The community consensus, corroborated across multiple analyses, is that “path-based policies can potentially be circumvented via hard links or mount tricks in ways label-based systems are not susceptible to,” whereas with SELinux “labels follow files regardless of path, so you cannot trick the system by moving a file — it keeps its label” (tuxcare). AppArmor has mechanisms to mediate link creation and (in newer versions) to reason about mounts, but the default mental burden is on the profile author to account for path aliasing; with SELinux the inode binding handles it structurally.

Uncertain

Verify: the precise extent to which current AppArmor (as shipped on 6.12/6.18-era Ubuntu/SUSE) mediates bind-mounts and hardlink-based path aliasing, and which mount/link mediation features are enabled by default. Reason: AppArmor has added mount, pivot_root, and link-mediation rule types over time, and the “path-based is bypassable” claim is partly historical; the exact default-enabled mediation on the pinned kernels was not pinned to a primary source during this research (apparmor.net’s own page and secondary analyses agree on the direction but not the current precise defaults). To resolve: read the AppArmor apparmor.d(5) mount/link rule sections for the shipped parser version and the distro’s default abstractions. #uncertain

Unconfined-by-default vs deny-by-default-within-confinement

The two have opposite default postures, and the nuance matters. AppArmor confines only programs that have a profile. Per apparmor(7), “a process that is already running unconfined cannot be confined,” and programs without a loaded profile run with no AppArmor restriction at all — equivalent to standard Linux discretionary access control. So on a stock AppArmor system the majority of binaries are unconfined; security comes from profiling the specific risky ones (a browser, a web server, a mail daemon). SELinux is deny-by-default — but only within the type-enforcement universe, and the targeted policy deliberately leaves most domains unconfined too. Inside a confined SELinux domain, anything not explicitly allowed is denied (deny-by-default). But the default targeted policy maps logged-in users and unprofiled services to unconfined_t, which is granted such sweeping allow rules that it behaves nearly unconfined — Red Hat’s docs note “processes that are not targeted run in an unconfined domain” (RHEL 7 targeted policy). So the honest framing is: both default to “confine the important services, leave the rest broad”, but the enforcement primitive differs — AppArmor’s unconfined programs have literally no MAC rules, while SELinux’s unconfined_t is still inside the deny-by-default TE engine, just with very permissive rules. The practical upshot AppArmor’s own page highlights: AppArmor profiles “are always enforced, regardless of the user context,” whereas SELinux’s targeted users are mapped to unconfined_u (apparmor.net).

Ease of authoring and auditing

AppArmor’s headline advantage is readability. Per AppArmor’s comparison, “AppArmor profiles are human-readable text files. Reviewing a profile is generally very easy, even for non-experts,” whereas “SELinux policies are compiled binary modules” that require audit-log analysis (AVC denials, audit2allow, sesearch) to troubleshoot, giving a “steeper” learning curve. AppArmor also ships a learning mode: per apparmor(7), a profile in complain mode “will not enforce policy [but] will report policy violation attempts,” and tooling (aa-genprof, aa-logprof, aa-complain/aa-enforce) turns those logged attempts into profile rules — generate-by-observation. SELinux has the analogous permissive-mode-plus-audit2allow loop (see SELinux Policy and Booleans), but the artifacts are denser. The trade is real: AppArmor lowers the barrier to writing some confinement; SELinux’s compiled, label-based model is harder but expresses constraints AppArmor’s per-path profiles cannot easily state.

Coverage and granularity

SELinux mediates more of the kernel. It “tends to take a system-wide perspective, striving for a clean mathematical model where every resource … has a label,” giving “system-wide granularity by design,” while AppArmor is “primarily designed for application confinement [and] effectively isolates specific services” (apparmor.net). Concretely, SELinux registers substantially more LSM hooks than AppArmor (one 2026-era analysis counted roughly 217 vs 80 hooks), so there are more operation classes SELinux can interpose on. AppArmor counters with lower overhead — “most micro-benchmarks and macro-benchmarks show that AppArmor introduces a lower overhead than SELinux” owing to fewer hooks and simpler policy evaluation.

Uncertain

Verify: the specific LSM-hook counts (≈217 SELinux vs ≈80 AppArmor) and the relative-overhead claim. Reason: the hook count came from a secondary web search citing “Linux v6.19” and was not confirmed against the v6.12/v6.18 source trees during this research; the overhead claim is AppArmor’s own and is workload-dependent. To resolve: count LSM_HOOK/security_* registrations in security/selinux/hooks.c vs security/apparmor/lsm.c for the pinned kernel, and cite a neutral benchmark for overhead. #uncertain


Side-by-Side Summary

DimensionSELinuxAppArmor
Naming modelLabel (type) bound to the inodePath of file + path of the binary the profile attaches to
Policy artifactCompiled binary policy (.pp/CIL → policy.NN)Human-readable text profiles in /etc/apparmor.d/
Default scopeSystem-wide type enforcement; targeted policy confines key services, rest unconfined_tOnly profiled programs confined; everything else unconfined
Rename / hardlink / bind-mountLabel follows the inode — structurally robustPath-based — needs careful profile authoring (see Uncertain above)
Authoring difficultySteep; AVC denials + audit2allow/sesearchEasy; readable profiles + complain-mode learning
Distro defaultRHEL, Fedora, CentOS, Rocky, AlmaLinuxUbuntu, Debian, SUSE
Both areAn exclusive label/path major LSM — only one can be the active TE/path-MAC(same)

Reading: the two columns differ on essentially every row, all of it tracing back to label-vs-path. The last row is the load-bearing constraint covered next.


You Cannot Run Both as the Major LSM

SELinux and AppArmor are both LSMs, and historically both were “major” / “exclusive” LSMs of the label-or-path-MAC kind that could not coexist — a system runs SELinux or AppArmor, not both. The reason is in Major vs Minor LSMs and LSM Stacking and Module Ordering: the LSM framework lets multiple minor modules (Yama, Landlock, the capabilities module, integrity modules) stack freely, but the comprehensive blob-owning MAC that wants to store its own per-object security label has, for most of LSM history, been limited to one active at a time, because before generalized LSM stacking only one module could own the inode/cred security blob. Even with the modern stackable-LSM work, the practical reality on shipping distributions through the 6.12/6.18 generation is that you pick one MAC: the lsm= boot ordering and the distribution’s build choose SELinux or AppArmor, and the other is simply not active. This is why the distro split is a fork in the road, not a menu — you do not get to layer AppArmor profiles on top of SELinux type enforcement.

Uncertain

Verify: whether, on the 6.12/6.18 LTS kernels, SELinux and AppArmor can be simultaneously active under the generalized LSM-stacking work, or whether they remain mutually exclusive “major” MACs in practice. Reason: LSM stacking has progressively relaxed the one-major-LSM rule, and a precise statement for these exact kernels needs the LSM-stacking documentation and the Kconfig/lsm= semantics confirmed against the source tree, which was not done in this research. The longstanding and almost-universal practical answer is “one or the other,” but the underlying kernel capability is evolving. To resolve: read Documentation/admin-guide/LSM/index.rst and the security/Kconfig LSM defaults for v6.12/v6.18, and check whether CONFIG_SECURITY_SELINUX and CONFIG_SECURITY_APPARMOR can both register as exclusive modules. See Major vs Minor LSMs and LSM Stacking and Module Ordering. #uncertain


When to Choose Which

In practice the choice is made for you by the distribution, and the strongest advice — echoed by every serious source — is use your distro’s default, because that is the one with maintained profiles/policy, vendor testing, and community support. On Red Hat–family systems run SELinux; on Ubuntu/Debian/SUSE run AppArmor. Fighting the default (installing SELinux on Ubuntu or AppArmor on RHEL) is possible but means inheriting an under-maintained policy set and far less support.

When you genuinely get to choose at the architecture level: prefer SELinux when you need comprehensive, system-wide mandatory access control, multi-level or category-based isolation (MLS/MCS — e.g. strong per-container/per-VM separation), and labels that cannot be subverted by path games; accept the steep authoring and auditing cost. Prefer AppArmor when you want to confine a handful of specific applications quickly, want profiles that a non-specialist can read and review, and value lower overhead and a gentle learning curve over total system coverage. The deeper principle: SELinux optimizes for completeness and rigor, AppArmor for approachability and per-app focus — and that is the same trade-off whether you are reasoning about security strength or about the human cost of operating the thing.


Production Notes

The real-world divide tracks the distro divide almost perfectly: organizations standardized on RHEL/Fedora operate SELinux (and live with setsebool, restorecon, audit2allow, and the occasional permissive-mode debug session), while Ubuntu/Debian shops operate AppArmor (and live with aa-status, complain-mode learning, and per-binary profiles in /etc/apparmor.d/). Both show up constantly in container troubleshooting: a Docker/Podman container that gets a baffling “permission denied” on a volume mount is usually hitting the host’s MAC — on SELinux hosts the fix is the :z/:Z relabel mount flag (so the container’s MCS category can access the volume), on AppArmor hosts it is the container runtime’s default profile (e.g. docker-default) denying a path. The uptake trend through the mid-2020s is that SELinux carries the larger enterprise/regulated-environment footprint (its completeness and MLS/MCS support matter for compliance), while AppArmor’s approachability keeps it dominant on developer-facing and Ubuntu-based infrastructure (linuxsecurity.com). Neither is a substitute for the other layers — both sit above discretionary access control and alongside capabilities, seccomp, and namespaces in a defense-in-depth stack (see Linux Security MOC).


See Also