SELinux vs. AppArmor: Mandatory Access Control Compared
AI generated
$
/etc
Linux · Security · SELinux · AppArmor
SELinux vs. AppArmor
Mandatory Access Control Compared

Standard Unix permissions do not stop a compromised process from reading everything the running user is allowed to read. Mandatory Access Control adds a second, kernel enforced barrier in front of that. This article compares SELinux and AppArmor, explains label based and path based policy with real examples, and shows how to reliably diagnose a MAC caused permission denied.

15 min read SELinux · AppArmor · audit2allow · aa-logprof RHEL · Ubuntu · Debian

1. What Mandatory Access Control adds beyond Unix permissions

Classic Unix permissions (rwx, owner, group, others) are Discretionary Access Control: the owner of a file decides on their own who can access it, and every process running as that user automatically inherits all of that user's rights. If a web server process gets compromised, an attacker can access every file the www-data user is allowed to read within the scope of that user's permissions, even if those files have nothing to do with the web server. This is exactly the gap that Mandatory Access Control (MAC) closes.

MAC adds a second, kernel enforced control layer that is not defined by the file owner but by a central security policy. Even if a process is technically allowed access under Unix permissions, the MAC policy can additionally deny that access. A compromised Nginx process with full root Unix rights can still be prevented, by a matching SELinux or AppArmor policy, from reading /etc/shadow or spawning a new shell. This containment, also called confinement, is the central value MAC systems add on top of plain Unix permissions.

Both major Linux MAC implementations, SELinux and AppArmor, build on the same kernel mechanism: Linux Security Modules (LSM), a hook layer that allows additional checks before every security relevant system call. The fundamental difference is not in how they attach to the kernel, but in how objects are identified and rules are formulated, as the following sections show.

2. SELinux: labels, contexts, and policy types

SELinux (Security-Enhanced Linux), originally developed by the NSA and the default on RHEL, CentOS, Fedora, and Rocky Linux, works in a label based way. Every file, process, port, and directory gets a security context in the form user:role:type:level, for example system_u:object_r:httpd_sys_content_t:s0. The policy decision is based almost exclusively on the type field: a process with type httpd_t may access files with type httpd_sys_content_t because a matching rule allows it, regardless of the actual file path.

This ground rule, called type enforcement, means a label is attached to the file, not the path. If a file is moved with mv to another directory, it keeps its old label, which often causes unexpected permission denied errors, while cp usually inherits a fresh label from the destination directory. Labels themselves are stored in the extended filesystem attribute security.selinux and are therefore tied to the filesystem, not to the kernel process.

SELinux has two policy types: targeted, the default on RHEL systems, which confines only selected, network exposed services, and strict, which confines every process on the system and is rarely used in practice today because the maintenance overhead is too high. The operating mode is configured separately in /etc/selinux/config: enforcing actively blocks violations, permissive only logs them, and disabled turns SELinux off entirely.


# Check current SELinux mode and policy type
sestatus
# SELinux status:                enabled
# SELinuxfs mount:                /sys/fs/selinux
# Current mode:                   enforcing
# Loaded policy name:             targeted

# Inspect the security context of a running process
ps -eZ | grep nginx
# system_u:system_r:httpd_t:s0   1204 ?  00:00:03 nginx

# Inspect the security context of files
ls -lZ /var/www/html/index.php
# -rw-r--r--. www-data www-data system_u:object_r:httpd_sys_content_t:s0 index.php

# A file moved with mv keeps its old, now wrong label
mv /home/deploy/upload.php /var/www/html/upload.php
ls -lZ /var/www/html/upload.php
# ...unconfined_u:object_r:user_home_t:s0   upload.php   <- wrong type, will be denied

# Fix: relabel according to the policy's default file context
restorecon -v /var/www/html/upload.php

3. AppArmor: path based profiles and modes

AppArmor, the default on Ubuntu, Debian, and SUSE, takes a fundamentally different approach: it is path based instead of label based. Instead of tagging files with invisible metadata, AppArmor defines a profile per application, a text file under /etc/apparmor.d/, that lists exactly which file paths are accessible with which rights (read, write, execute). The profile name usually matches the full path to the binary, for example /etc/apparmor.d/usr.sbin.nginx.

This approach has a decisive practical advantage: profiles are human readable plain text you can edit directly without having to reset labels. The downside shows up with symlinks and bind mounts: because the rule is tied to the path, a cleverly placed symlink pointing to an alternate target can under certain conditions bypass a path rule, an attack vector that label based systems like SELinux tend to avoid by binding to the actual object.

AppArmor has two central modes per profile: enforce, which actively blocks violations, and complain (the counterpart to SELinux permissive), which only logs violations without preventing them. Unlike SELinux, which mostly operates in a single system wide mode, AppArmor lets you set the mode per profile individually: a newly introduced service can run in complain while established services remain in enforce, which makes gradual rollout considerably easier.


# List loaded profiles and their current mode
aa-status
# apparmor module is loaded.
# 34 profiles are loaded.
# 30 profiles are in enforce mode.
#    /usr/sbin/nginx
#    /usr/sbin/mysqld
# 4 profiles are in complain mode.
#    /usr/sbin/dovecot

# Example profile excerpt: /etc/apparmor.d/usr.sbin.nginx
#include <tunables/global>
/usr/sbin/nginx {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  capability net_bind_service,
  capability setuid,
  capability setgid,

  /etc/nginx/**            r,
  /var/www/html/**         r,
  /var/log/nginx/*.log     w,
  /run/nginx.pid           rw,
  deny /home/**            rwx,
}

# Switch a profile into complain mode for a low-risk rollout
aa-complain /usr/sbin/nginx

# Switch it back into enforce mode once the profile is validated
aa-enforce /usr/sbin/nginx

4. Architecture comparison: label vs. path

The core difference between the two systems shows up most clearly with filesystem operations. SELinux labels are stored as an extended attribute on the inode and therefore survive mv within the same filesystem (with a now wrong, stale label), but get lost if the filesystem does not support extended attributes, for example on some NFS exports or tmpfs mounts without matching mount options. AppArmor path rules do not have this problem, because they are evaluated live against the current path on every system call. In exchange, they carry a different weakness: bind mounts and cleverly placed symlinks can bypass a path rule under certain conditions, while an SELinux label stays attached to the actual object.

Operationally, the two worlds also differ considerably. SELinux policy is compiled from type definitions and CIL rules, maintained with semanage, restorecon, and policy modules, and requires a steeper learning curve at first, but in return offers Multi-Level Security (s0 through s0:c0.c1023) as an additional classification layer for highly sensitive multi tenant environments. AppArmor profiles are often quicker to pick up for developers who are used to text configuration, but lack a comparable categorization layer. In the container world, Docker and Podman on RHEL hosts use SELinux types like container_t for isolation between containers, while LXD and Snap packages on Ubuntu hosts rely on automatically generated AppArmor profiles per container.

5. Diagnosis: finding an SELinux caused permission denied

The typical symptom: a process gets Permission denied, even though ls -l clearly shows that owner, group, and mode should allow the access. Checking /var/log/messages or journalctl usually turns up nothing in this case, because SELinux violations, so called AVC denials (Access Vector Cache), end up in their own log file: /var/log/audit/audit.log. The targeted way to get there is ausearch -m avc -ts recent, which filters out only the relevant recent AVC entries instead of scrolling manually through a huge audit log.

An AVC entry contains scontext (context of the accessing process), tcontext (context of the target object), and tclass (object class such as file or tcp_socket). These three fields usually reveal immediately which rule is missing. If setroubleshoot is installed, sealert -a /var/log/audit/audit.log additionally provides a plain language explanation along with a concrete fix suggestion, which considerably speeds up troubleshooting for teams without daily SELinux practice.


# Search recent AVC denials instead of scrolling raw audit.log
ausearch -m avc -ts recent

# Example AVC denial line
# type=AVC msg=audit(1720771200.123:456): avc:  denied  { read } for
#   pid=1204 comm="nginx" name="upload.php" dev="dm-0" ino=459812
#   scontext=system_u:system_r:httpd_t:s0
#   tcontext=unconfined_u:object_r:user_home_t:s0
#   tclass=file permissive=0

# Human-readable explanation and fix suggestion
sealert -a /var/log/audit/audit.log

# Generate a minimal policy module from the last denial (review before loading!)
ausearch -m avc -ts recent | audit2allow -M nginx_upload_fix
semodule -i nginx_upload_fix.pp

6. Diagnosis: finding an AppArmor caused permission denied

With AppArmor, violations do not end up in a dedicated audit log but go straight into the kernel ring buffer, visible via dmesg or journalctl -k. The telltale line contains apparmor="DENIED", followed by the affected profile, the path, and the requested operation. Anyone chasing a mysterious Permission denied who first checks ls -l and the Unix permissions and finds nothing unusual there should, as the next step, filter specifically for this line instead of searching the entire system log.

Once the culprit is identified, aa-status shows which mode the responsible profile is currently running in. For the actual fix, aa-logprof is the central tool: it interactively reads the recorded denials and suggests an addition for each missing rule, which you can confirm, adjust, or reject individually. For completely new applications without an existing profile, aa-genprof, combined with real usage of the program, generates a starter profile in complain mode.


# Filter kernel log for AppArmor denials
journalctl -k --since "10 min ago" | grep apparmor

# Example denial line
# audit: type=1400 audit(1720771300.456:78): apparmor="DENIED"
#   operation="open" profile="/usr/sbin/nginx"
#   name="/home/deploy/uploads/report.pdf" pid=1204 comm="nginx"
#   requested_mask="r" denied_mask="r" fsuid=33 ouid=1001

# Check which mode the responsible profile is currently running in
aa-status | grep -A2 nginx

# Interactively update the profile based on captured denials
aa-logprof

# Reload the updated profile without a reboot
apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx

7. Adjusting policy instead of disabling enforcement wholesale

The most common mistake under time pressure: setenforce 0 or systemctl stop apparmor, to quickly get a blocked deploy running again. The problem is that these commands do not just bypass the one missing rule, they disable the entire protection layer for the whole system or all profiles at once, often indefinitely, because nobody remembers to turn it back on. The correct approach is always to add the specific missing exception rather than switching off the whole control layer.

For SELinux this means reading the policy generated by audit2allow before loading it with semodule -i: if the source contains several unrelated denials from different incidents, the generated module can unintentionally grant overly broad rights. For permanent path changes, semanage fcontext -a -t httpd_sys_content_t "/srv/app(/.*)?" followed by restorecon -Rv /srv/app is the correct approach, one that survives reboots and repeated relabeling. The same applies to AppArmor: use aa-logprof instead of deleting a profile outright, and review every suggested rule individually before accepting it, so the profile does not gradually turn into a meaningless blanket allow.

8. Enforcing, permissive, or disabled: the decision per environment

On publicly reachable production servers, MAC should practically always run in active mode, SELinux enforcing or AppArmor profiles in enforce, because these systems are the primary attack target and the extra effort of maintaining policy is justified by the security gain. Many compliance frameworks such as PCI-DSS or ISO 27001 already require active access control beyond plain Unix permissions, which effectively makes enforcing mode mandatory in regulated environments. Local development environments, on the other hand, can temporarily run in permissive or complain mode to avoid slowing down iteration speed while a new service is being introduced, but must switch to the same mode as the target environment before a production deploy.

Fully disabling SELinux (disabled in /etc/selinux/config), unlike switching between enforcing and permissive, additionally requires a reboot, because the kernel initializes the security structure at boot time. This operational hurdle is deliberately set high and should be understood as a warning sign: whoever needs disabled regularly usually has a policy problem, not a fundamental SELinux problem. Realistically, disabled only makes sense for isolated special cases, such as short lived throwaway test environments with no production relevance whatsoever.


# Ansible task: enforce MAC baseline differently per environment tier
- name: Set SELinux mode based on environment tier
  ansible.posix.selinux:
    policy: targeted
    state: "{{ 'enforcing' if env_tier == 'production' else 'permissive' }}"
  when: ansible_os_family == "RedHat"

- name: Ensure AppArmor profile is enforced in production
  command: aa-enforce /etc/apparmor.d/usr.sbin.nginx
  when:
    - ansible_os_family == "Debian"
    - env_tier == "production"

- name: Keep AppArmor profile in complain mode on staging
  command: aa-complain /etc/apparmor.d/usr.sbin.nginx
  when:
    - ansible_os_family == "Debian"
    - env_tier == "staging"

9. SELinux and AppArmor in direct comparison

Both systems solve the same underlying problem with different means, and in practice you rarely choose between them because the Linux distribution usually dictates the decision. What matters day to day is avoiding the typical wrong reaction to a denial and fixing it in a targeted way instead.

Situation Risky reaction Recommended approach Benefit
SELinux blocks unexpected access Permanently run setenforce 0 Generate and review a targeted audit2allow policy Protection stays active, only the needed exception is allowed
AppArmor blocks a new service Delete the profile or aa-disable Set the profile to complain and use aa-logprof Confinement stays prepared, fine tuning without downtime
File unreachable after mv chmod 777 as a workaround restorecon -v for the correct label Root cause fixed instead of just hidden
Setting up a new environment Disable MAC wholesale via a template targeted/enforce as the default, exceptions documented No silent security regression on new hosts
Tracking down a denial Randomly grep the syslog for "denied" Filter targeted with ausearch -m avc or journalctl -k Precise context match instead of a lucky guess

Mironsoft

Server hardening, security audits, and deployment infrastructure

SELinux or AppArmor denials blocking your deploy?

We analyze AVC denials and AppArmor profiles, generate targeted policy exceptions instead of disabling enforcement wholesale, and make sure your servers stay production hardened without blocking deployments.

Denial analysis

Evaluate audit.log and dmesg in a targeted way and identify the actual root cause

Policy hardening

Minimally invasive SELinux modules and AppArmor profiles instead of blanket deactivation

Rollout strategy

Gradual transition from permissive/complain to enforcing without production downtime

10. Summary

SELinux and AppArmor solve the same underlying problem, that Unix permissions leave a compromised process too much room, with different architecture. SELinux assigns labels to files, processes, and ports and decides based on type enforcement, independent of the file path, which is particularly robust against symlink tricks but comes with a steeper learning curve. AppArmor works path based with human readable profiles under /etc/apparmor.d/, is quicker to pick up, but theoretically more vulnerable to clever path manipulation.

For diagnosis, the same basic rule applies in both worlds: a Permission denied despite correct Unix permissions almost always points to a MAC denial, visible in /var/log/audit/audit.log for SELinux or in the kernel log via dmesg/journalctl -k for AppArmor. Instead of setenforce 0 or disabling a profile entirely, the specific missing rule should always be added via audit2allow or aa-logprof. On publicly reachable production systems, active mode, enforcing or enforce, is part of the security baseline, while local development environments can temporarily run in logging mode.

SELinux vs. AppArmor: the essentials at a glance

Label vs. path

SELinux decides based on file labels and type enforcement, AppArmor based on file paths in readable profiles.

Diagnostic source

SELinux denials live in audit.log (ausearch -m avc), AppArmor denials in the kernel log (dmesg, journalctl -k).

Fix instead of disable

audit2allow and aa-logprof add missing rules in a targeted way instead of disabling enforcement wholesale.

Mode per environment

Production: enforcing/enforce as the default. Development: permissive/complain temporarily acceptable.

11. FAQ: SELinux vs. AppArmor

1What is the difference between DAC and MAC?
DAC (Unix permissions) lets the file owner decide. MAC adds an additional, kernel enforced layer that can deny access even when it would otherwise be allowed.
2Is SELinux or AppArmor more secure?
Both provide effective confinement. SELinux is more robust against path manipulation, AppArmor is easier to maintain. The choice usually depends on the distribution.
3File blocked after mv, why?
The SELinux label stays old after moving and no longer fits in the new directory. restorecon -v resets the correct label.
4Finding an SELinux denial?
ausearch -m avc -ts recent in audit.log. sealert -a /var/log/audit/audit.log additionally provides a plain language explanation.
5Finding an AppArmor denial?
Search journalctl -k or dmesg for apparmor="DENIED". The line shows the profile, path, and requested operation.
6Just disable it when there are problems?
No. That disables the whole protection layer instead of just the one missing rule. Fix it in a targeted way with audit2allow or aa-logprof.
7What does audit2allow do?
Generates a policy module from AVC denials. Always review manually before loading with semodule -i to avoid overly broad permissions.
8What does aa-logprof do?
Interactively reads AppArmor denials and suggests missing rules that can be confirmed individually. aa-genprof generates starter profiles for new applications.
9SELinux and AppArmor together?
Technically partially possible, uncommon in practice. Distributions ship exactly one by default, and switching means considerable migration effort.
10Migrating to enforcing without downtime?
Run in logging mode longer first, convert denials into policy additions, test under real load, only then switch to active mode.