Skip to content

SELinux and AppArmor

Ordinary Unix permissions answer one question: does this user have the right to read, write or execute this file? Every program a user runs inherits all of that user's rights. A web server running as www-data can read anything www-data can read, and a compromised web server running as root can do anything at all. The owner of a file can also give those rights away: chmod 777 is always one command away. This model is called discretionary access control (DAC), because access is at the owner's discretion.

Mandatory access control (MAC) adds a second, independent check, based on a policy that the administrator (or the distribution) loads into the kernel and that ordinary users, and even root processes, can't change from inside a confined program. The policy describes what a program is supposed to do: the web server may read its content and its configuration, bind to ports 80 and 443 and write its logs, and nothing else. If an attacker takes over the process, the kernel still refuses everything outside that description, even when the Unix permissions would allow it.

SELinux and AppArmor are the two MAC systems in common use on Linux servers. They aim at the same result but describe the world very differently.

Where they sit in the kernel

Both are Linux Security Modules (LSMs). The kernel calls LSM hooks at the points where it makes security decisions: opening a file, executing a program, creating a socket, sending a signal. A request has to pass the normal permission checks and every active LSM; any one of them can refuse it, and none of them can grant something the Unix permissions deny.

flowchart LR
    p["Process calls open()"] --> dac{"Unix permissions<br/>and capabilities"}
    dac -->|denied| e1["EACCES / EPERM"]
    dac -->|allowed| lsm{"Active LSMs<br/>SELinux or AppArmor,<br/>plus Landlock, Yama, …"}
    lsm -->|denied| e2["EACCES, and an AVC<br/>or apparmor= audit record"]
    lsm -->|allowed| ok["File opened"]

The list of active LSMs is visible in /sys/kernel/security/lsm. Distribution kernels usually have both SELinux and AppArmor built in, and the boot configuration decides which is switched on. In practice a system runs one of the two, never both; smaller modules such as Yama (which restricts ptrace), Landlock (self-imposed sandboxing) and BPF-based policies are stacked alongside it.

A refusal from an LSM looks to the program exactly like a permission error: Permission denied. That's the most common reason MAC confuses people. The file mode says the access is allowed, and it is refused anyway; the reason is only in the audit log.

SELinux: labels on everything

SELinux gives every process and every object (file, directory, socket, port, device) a security context, a label of the form user:role:type:level:

system_u:system_r:httpd_t:s0                 a process: the Apache or nginx web server
system_u:object_r:httpd_sys_content_t:s0     a file: web content under /var/www/html
system_u:object_r:user_home_t:s0             a file: something in a home directory

The part that matters day to day is the type: httpd_t, httpd_sys_content_t, user_home_t. The policy is mostly a long list of rules saying which process types may do what to which object types, for example "httpd_t may read files of type httpd_sys_content_t". Anything not allowed is denied. This is type enforcement. The level (s0) belongs to multi-level security, which the default targeted policy hardly uses.

A file's label is stored with the file, in an extended attribute, and it travels with the inode, not with the name. That has consequences that surprise people coming from AppArmor:

  • A file moved with mv keeps the label it had in its old location. A page written in a home directory and moved to /var/www/html is still user_home_t, and the web server can't read it.
  • A file copied with cp gets a new label from the directory it lands in.
  • The "correct" label for a path is defined by file context rules in the policy (/var/www(/.*)? is httpd_sys_content_t), and restorecon resets files to what those rules say. Local additions are made with semanage fcontext.

The same labelling applies to non-file objects. A web server may bind only to ports labelled http_port_t, so running it on port 8081 needs that port added to the label (semanage port), not a change to the server. Many common variations are anticipated by booleans, named switches in the policy such as httpd_can_network_connect or httpd_enable_homedirs, which turn a group of rules on or off without writing policy.

The RHEL family ships the targeted policy: most system services run in their own confined domains, while users' logins and anything the policy doesn't know about run in the unconfined_t domain, where SELinux allows nearly everything. So SELinux on a default RHEL system protects services from being turned against the rest of the system, rather than restricting what an administrator types.

AppArmor: profiles keyed by path

AppArmor attaches a profile to a program, chosen by the path of the executable, and the profile lists what that program may access, again by path:

/usr/sbin/example-daemon {
  #include <abstractions/base>
  /etc/example/** r,
  /var/lib/example/** rw,
  /run/example.pid w,
  network inet stream,
}

Profiles live as text files in /etc/apparmor.d/, named after the program they confine, and are loaded into the kernel with apparmor_parser. A program with no profile is unconfined: AppArmor doesn't restrict it at all. Policy is therefore opt-in per program, which keeps AppArmor small and easy to reason about: to find out what a confined program may do, you read one file, in terms you already know.

Because rules are written against paths, nothing has to be relabelled when files are created, moved or restored from backup, and there's no filesystem support requirement. The flip side is that the same file reached by two different paths (a hard link, a bind mount) can be treated differently, and a profile written for /var/www says nothing about content you put somewhere else.

Debian and Ubuntu enable AppArmor by default and ship profiles for a selection of programs; Ubuntu enforces many more of them, and also uses AppArmor to restrict which applications may create unprivileged user namespaces. Packages often add their own profiles when installed.

Modes

Both systems can enforce a policy (deny and log) or only report what they would deny.

SELinux AppArmor
Enforcing enforcing: the whole system enforce mode, per profile
Logging only permissive: the whole system, or one domain with semanage permissive complain mode, per profile
Off disabled: no labels are maintained while it's off Profile unloaded, or AppArmor not enabled at boot
Where it's set /etc/selinux/config and the kernel command line; setenforce switches at run time Profile flags, aa-enforce and aa-complain; apparmor= and lsm= on the kernel command line

Permissive or complain mode is a diagnostic tool: it lets you collect every denial an application would cause, so you can fix the policy or the labels in one go, then return to enforcing. Leaving a system permissive, or disabling MAC to "fix" a problem, removes the protection entirely while hiding the actual cause, which is usually a single mislabelled file or a missing boolean. Since RHEL 9, SELINUX=disabled in /etc/selinux/config no longer turns SELinux off at all (it only stops a policy from loading); disabling it takes the selinux=0 kernel parameter. SELinux disabled is worse than permissive: files created while it's off get no labels, so turning it back on later needs a full relabel of the filesystem at boot.

Which distributions use which

What each lab image of the distributions covered here has active by default, as reported by /sys/kernel/security/lsm:

Distribution Active by default Notes
Debian 13 AppArmor, enforcing a small set of profiles (seven in the lab image) Package apparmor; more profiles in apparmor-profiles and apparmor-profiles-extra
Ubuntu 26.04 LTS AppArmor, enforcing many profiles (about a hundred in the lab image) Also restricts unprivileged user namespaces (kernel.apparmor_restrict_unprivileged_userns = 1)
AlmaLinux 10, Rocky Linux 10 (and RHEL 10) SELinux, targeted policy, enforcing AppArmor isn't built into the kernel
Arch Linux Neither The stock kernel has both built in; either one is enabled with the lsm= kernel parameter and the userspace packages
Gentoo (OpenRC) Neither Depends entirely on the kernel configuration and profile chosen; Gentoo has SELinux profiles and supports AppArmor

Choosing against the distribution's default is possible but expensive. SELinux on Debian or AppArmor on a RHEL rebuild works only as well as the policy you maintain yourself; the distributions' own policies are tested together with their packages, and that testing is most of the value.

Trade-offs

SELinux is more expressive and more thorough. Labels cover files, processes, ports, sockets and IPC, and type enforcement can say things paths can't, such as "content written by this daemon may never be executed by that one". The targeted policy confines hundreds of services out of the box. The cost is complexity: denials are phrased in types you have to learn, labels need maintaining when files are moved or restored, and writing new policy is a specialised skill.

AppArmor is easier to read, write and audit, and needs no labels. Its coverage is narrower: only programs with a profile are confined, and path-based rules are weaker against tricks that create alternative names for a file. For confining a handful of exposed services on a server, that is often enough.

Either one, enforcing, is far better than neither. Most attacks against a server go through a network service, and MAC is what limits the damage when that service is compromised, whatever the Unix permissions say. That is also why it's worth learning to read denials rather than switching enforcement off: the troubleshooting guide shows how.

Further reading