Alright, let’s cut through the noise. You’ve been using Discretionary Access Control (DAC) your entire computing life, even if you didn’t know its official name. It’s the model where access to objects (files, sockets, etc.) is based on the identity of the user and the groups they belong to. The classic rwx permissions. The “discretionary” part is the problem: if you own a file (userA), you can discretionarily change its permissions to chmod 777, making it world-readable and writable. This is bonkers from a security perspective. A single misconfigured script or a compromised user process can blow the doors off your entire system.

Mandatory Access Control (MAC) is the stern, unyielding librarian to DAC’s chaotic, “you can just have my house keys” approach. With MAC, the system defines the security policy, not the user. Even if you’re root (yes, really), you cannot violate this policy once it’s set. Your ability to access something isn’t just about who you are; it’s about what you’re doing and what label the system has assigned to that process and that object. The goal is containment. If the nginx process gets owned by an attacker, MAC policies aim to prevent that process from doing anything it wasn’t explicitly allowed to do, like reading your SSH keys or formatting your disk. It’s a phenomenal second line of defense.

The Core Difference: Discretionary vs. Mandatory

Think of it like this. In a DAC world (standard Linux permissions), if I, as the owner (userA) of secret_recipe.txt, give you (userB) permission to read it, that’s it. You can read it. You can also probably copy it, email it to the world, whatever. I discretionarily granted you access, and now the security of that file is, frankly, your problem.

In a MAC world, the system would have a rule that says, “Processes running as userB are only allowed to read files with the label user_b_files.” Even if I, userA, use DAC to chmod 777 secret_recipe.txt, it doesn’t matter. My file has the label user_a_secrets. When you, userB, try to read it, the MAC system (SELinux or AppArmor) intercepts the request, checks its policy, sees that your process isn’t allowed to access objects with the user_a_secrets label, and denies it even though the standard Linux permissions say it’s okay. The system mandates this denial. This is the superpower.

How SELinux Implements MAC: Labels and Policies

SELinux is the more complex, all-encompassing beast. It uses a system of labels, called contexts, applied to every single object on the system—processes, files, ports, everything. You can see these labels with ls -Z and ps -Z.

$ ls -Z /etc/passwd
system_u:object_r:passwd_file_t:s0 /etc/passwd

$ ps -Z -C nginx
system_u:system_r:httpd_t:s0    pid?   tty?    time?   /usr/sbin/nginx

The magic is in the third part: the type. Here, the file has type passwd_file_t and the process has type httpd_t. The SELinux policy is essentially a gigantic list of rules defining what a source type (like httpd_t) is allowed to do to a target type (like passwd_file_t). A rule might say “httpd_t is allowed to read passwd_file_t.” If the action isn’t explicitly allowed, it’s denied. This is called Type Enforcement.

Why this complexity? Granularity. You can have ten different web server processes all running as the same user but with different SELinux types, each with a uniquely tailored set of permissions, preventing a compromise in one from affecting the others.

How AppArmor Implements MAC: Paths and Profiles

AppArmor takes a different, arguably more pragmatic, approach. It ditches the complex labeling system for something simpler: path-based profiles. Instead of worrying about what label a file has, an AppArmor profile defines what paths a confined process can access.

An AppArmor profile for nginx would have rules like:

# /etc/apparmor.d/usr.sbin.nginx
/usr/sbin/nginx {
  # Include abstractions for common paths
  #include <abstractions/base>
  #include <abstractions/apache2-common>

  # Allow reading static web content
  /var/www/html/** r,

  # Allow reading SSL certificates
  /etc/ssl/certs/** r,

  # Deny everything else by default
  deny /etc/passwd r,
  deny /tmp/** w,
}

AppArmor’s philosophy is “whitelist what you know.” It’s easier to reason about because it deals with the concrete paths you already know, not abstract labels. The trade-off is that it’s arguably less flexible for incredibly complex scenarios, but for 95% of use cases, it’s brilliantly effective and far easier to manage.

The Most Common Pitfall: Your Brain vs. Their Policy

The number one reason people “disable that SELinux nonsense” is audit denials. Something doesn’t work, they check /var/log/audit/audit.log or journalctl, see an AVC denial message, and instead of understanding it, they throw their hands up and set SELinux to permissive mode. You’ve just turned your stern librarian into a polite commentator who notes rules violations but does nothing to stop them. Don’t do this.

The correct tool is audit2why (on SELinux systems) to translate those obtuse denial messages into human-readable explanations and suggested fixes.

$ sudo grep AVC /var/log/audit/audit.log | tail -1 | audit2why
# Example output:
# The nginx process (httpd_t) tried to read a file labeled user_home_t.
# This is not allowed by policy. If you intend this, you need to add a rule
# to allow httpd_t to read user_home_t. You can use semanage to change the
# context on the file, or boolean httpd_read_user_content to allow this broadly.

See? Not so bad. It often even tells you the exact setsebool or semanage fcontext command to run to fix it properly. AppArmor users have similar tools like aa-logprof to analyze denials and update profiles. The pitfall isn’t the technology; it’s our impatience.