SELinux and Apparmor Overview

Mandatory Access Control (MAC) on Linux desktops


I’ve been using GNU/Linux distributions as my daily driver OS on desktop PCs and notebooks for more than 10 years. Linux is a great choice to me, because like most FOSS it values my privacy, it’s fun to use because of its customization options and because it is secure. Because it’s secure, right?


If you thought you are better off with e.g Linux Mint than those Mac OS and Windows peasents beause of it’s superiority in terms of security… well, just read this article by madaidan or watch this (German) talk by lubro. I’m not going to get into whether all the points are 100% fair and have that much of an impact, but a couple of points are clearly a problem with common Linux distributions, that should be fixed:

  • Lack of (properly configured) MAC (mandatory access control)
  • Lack of verified boot and often unencrypted boot partitions
  • Legacy code without security in mind (e.g. X11 or PulseAudio)
  • Lack of (consistent) sandboxing

The major reason why you’re better off is mainly because 2023 is still not the Year of the Linux desktop™ and malware developers are focused on office environments with Windows clients and AD controllers. They just don’t care about our snowflake operating systems. Another reason might be the fact, that Linux users tend not to suffer as much from layer 8 problems as the average user. It takes some basic unterstanding of how computers work to come to the conclusion that you want to use Linux and customize it to your needs. So we all don’t do stupid things like chmod 777 or pipe wget or curl to shell, right? Also don’t forget advantages like package management, discretionary access control (POSIX-compliant file permissions), publicly available source code and auditing, livepatching of the kernel and so on.


My main goal is to harden my Linux based operating system in a way, which does not render it unusable, but migitates the biggest risks. This article is intended to give a basic overview about MAC and the usage on curren Linux desktops. I also plan posts regarding full-disk encryption and a solution to a more secure boot (pun intended). I am also considering to write a practical guide to show how it’s done in a real world example.


All of this depends on your threat model. You are following this guide at your own risk, I am not liable for any loss or damage you suffer!


Linux is a unix-like operating system, this is reflected, among other things, in the following characteristics:

  • Everything is a file
  • There are file permissions

The file permissions in GNU/Linux distributions are a form of discretionary access control (DAC). Every file has its owner, who can change the permissions of that file. Those permissions are divided into three classes: Owner, group and other. The group is a group of users, where the owner does not necessarily need to be member of that group. The other class describes the permissions for every user that is not the owner and not in the group. The Linux kernel has exceptions to that for privileged users (usually root), for example CAP_DAC_OVERRIDE. That means the file system permissions don’t apply to the root user except the executable bit, which can of course be flipped by root.

The POSIX standard (IEEE P1003.1) allows additional file access control mechanisms:

An implementation-defined mechanism that is layered upon the access control mechanisms defined here, but which do not grant permissions beyond those defined herein, although they may further restrict them.

That means, POSIX-compliant MACs can only restrict the file permissions further, they cannot grant rights, that weren’t there. The Linux kernel always checks the file permissions first and only if they allow the action, the MAC is asked.

Why MAC, when we have DAC?

The problem: Servers generally have unprivileged users for different tasks, while desktop computers usally have just one user. This one user runs all the applications, so all applications have the same file permissions of said user. That means: Your browser is able to access your ssh keys, your mail client can read your KeePass database and your pdf reader can write to dot files like .bashrc and others. This is a serious threat, because one compromised application is able to fuck up your whole system. Your main user being in the wheel group or having sudo privileges even worsens things.

Luckily the Linux kernel officially supports four security modules to implement MAC: AppArmor, SELinux, Smack, and TOMOYO Linux. AppArmor and SELinux are the most widespread implementations, as Red Hat derivatives ship SELinux and AppArmor is shipped by Ubuntu, Debian and openSUSE, among others.

But how do they work? You define profiles (AppArmor) or policies (SELinux) to define what your applications really need to access. (That’s the best practice, in theory it is possible to allow everything and then blacklist things, but why would you want that?)

Q: So we’ll just need to install a modern distribution like Fedora or Ubuntu and we are good to go?

No. Even though SELinux ist developed by Red Hat and Canonical contributes a lot to AppArmor, most of the programs run by the main user are unconfined. To be fair: On Red Hat distributions at least most of the daemons run by root are confined, which shows that security on Linux primarily focuses on servers.


To understand SELinux you’ll just need to know Dan Walsh’s prayer:

  • SELinux is a labeling system
  • Every process has a label
  • Every file, directory, system object has a label
  • Policy rules control access between labeled processes and labeled objects

You are also able to label network ports. So if you want to change the sshd port of your server, you have to tell SELinux about it, because it will block it otherwise. SELinux also supports booleans, which allow you to toggle certain polices at runtime.

Type enforcement

SELinux works mainly on type enforcement. That means users, processes, and objects get labels, which are called type in the SELinux world. Extremely simplified: An SELinux policy allows users with the type videowatcher_t to access files with the type video_t thanks to the following policy:

allow videowatcher_t video_t:file read;

Multi-Level Security (MLS)

MLS works like security clearences in intelligence agencies and military, maybe the NSA wanted and implemented that feature, because nobody in the real world uses that feature. The MLS in SELinux is context based. Contexts are based on a user, a role and a type (and optionally a level). What does that mean? First of all: An SELinux user is not a user account, but user accounts are mapped to one SELinux user (n:1). An SELinux user can have multiple roles which lead to types.

So we maybe have the user pekka on our system and map his user to the SELinux user user_u, which has the role media_r which contains the type videowatcher_t. There is a file called rare_spurdo_meme.mp4 on his computer with the following context:

pekka@euroshopper /dank_memes/rare> ls -Z
memelord_u:media_r:video_t:s0 rare_spurdo_meme.mp4

Without MLS pekka would be able to open rare_spurdo_meme.mp4, because we allowed it in our previous example. The domain type videowatcher_t is allowed to read files with the type video_t. But if we activate MLS and the label memelord_u has a higher clearence than user_u, he will be unable to watch the video.

Multi-Category Security (MCS)

MCS is another additional feature and works as an additional label. The explanation by Red Hat is pretty straightforwarded:

MCS works on a simple principle: to access a file, a user must be assigned to all of the categories that have been assigned to the file. The MCS check is applied after normal Linux Discretionary Access Control (DAC) and SELinux Type Enforcement (TE) rules, so it can only further restrict existing security configuration.

The real world

Q: That all sounds great! It works with whitelists and denies everything else. So where is the problem?

Time for a real world example on my Fedora workstation:

spurdo@euroshopper ~> ls -Z ~/.ssh
unconfined_u:object_r:ssh_home_t:s0 config
unconfined_u:object_r:ssh_home_t:s0 id_ed25519 
unconfined_u:object_r:ssh_home_t:s0 known_hosts

With ls -Z we are able to see the security context of each file in a directory. So that’s great: The contents of .ssh are actually labeled by default

spurdo@euroshopper ~> ps -Z 5178
LABEL                               PID TTY      STAT   TIME COMMAND
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 5178 ? Sl   0:14 /usr/lib64/firefox/firefox

So far so good. An unconfined process won’t be able to access a confined directory, right? Wrong. It’s allowed on most operating systems with SELinux by default:


Ok, if you read the linked carefully it is not that bad anymore, since SELinux developers added some limited confinement to the unconfined domain like preventing users to execute both writable and executable. They also clustered the permissions and wrapped them into booleans which enables administrators to confine the unconfined domain even more. But let me quote Dan Walsh again:

If a cracker gets into an SELinux box as unconfined_t, SELinux will prevent him from doing very little, since that is how unconfined_t is designed. It is up to DAC to protect the system.


AppArmor on the other hand is not based on labels, but on paths. You simply define profiles for each application by providing its path and restricting the paths the application has access to. It also restricts network access, capabilities and resource limits. Here’s avery simple example I copied from a talk by Christian Boltz for a simple hello world script:

echo "Hello World!" > /tmp/hello.txt
cat /tmp/hello.txt
rm /tmp/hello.txt

Fixed filenames in /tmp can be abused for symlink attacks. The following AppArmor profile would prevent this:

# Last Modified: Sat Jul 21 14:42.45076019 +0200
#include <tunables/global>

/home/spurdo/ {
  #include <abstractions/base>
  #include <abstractions/bash>

  /dev/tty rw,
  /home/spurdo/ r,
  /usr/bin/bash ix,
  /usr/bin/cat mrix,
  /usr/bin/rm mrix,
  owner /tmp/hello.txt rw,


Now we can create an evil symlink:

ln -s /home/spurdo/.ssh/authorized_keys /tmp/hello.txt

Even running the script as root will result in ‘Permission denied’.

The workflow to generate profiles is easier than in SELinux. In SELinux you will confine an application by a new policy, run into denials and then work your way through the denials with audit2allow. AppArmor on the other hand has a tool called aa-genprof which will generate a profile and sets it into complain mode, which will log everything that would be denied without denying it. When you are done, it will ask you action by action, how the policy should handle it.

AppArmor Profiles

Ubuntu has a collection of AppArmor Profiles in their package apparmor-profiles including GUI applications like Firefox or Pidgin. Besides the profiles from the AppArmor developers OpenSUSE ships also individual profiles together with the relevant applications. Those profiles aren’t enabled by default and reside in /usr/share/apparmor/extra-profiles.

That means no MAC by default for most applications. We have the same problem on common AppArmor distributions as on SELinux distributions: Lack of policies/profiles. The apparmor.d project tries to tackle this problem. They try to generate a big collection of distribution agnostic AppArmor profiles to confine the following:

  • All root processes such as all systemd, bluetooth, dbus, polkit, NetworkManager, OpenVPN, GDM, rtkit, colord
  • Desktop environments: Gnome, KDE
  • All user services such as Pipewire, Gvfsd, dbus, xdg, xwayland, xorg
  • All “sandbox managers”: flatpak, snap, docker, podman, toolbox, libvirt, steam…
  • Some “special” user applications: web browser, file browser…

(Quoted from Alexandre Pujol – Building the largest working set of AppArmor profiles, page 7)

I have been searching for a similar SELinux project, but besides the SELinux reference policy and its Fedora fork found only Gentoo’s SELinux project which provides a big collection of SELinux policies. Their policies even protected their users from a real-world firefox CVE.


SELinux and AppArmor are somehow like a powerful firewall: They can only be as good as their policies. But sadly the default policies on most desktop operating systems suck ass. Wonder why? Because SELinux is not that easy to learn and polices are hard to write. Red Hat did a great job in confining all the services running on servers, but not on desktop operating systems. Users can use audit2allow to generate suggestions for policies based on the AVC (Access Vector Cache) logs of SELinux. So it is certainly possible to confine applications on your own, but the whole process looks painful.

AppArmor is way easier to understand than SELinux thanks to its simplicity. Confinement based on paths in profiles is not as effective as labeling everything – on a SELinux enabled system, everything is labeled on inode level. On the other hand we’ll see unconfined_t on real SELinux systems over and over again, so it can really only take advantage of this if the system is consistently provided with labels other than unconfined_t. I really like the simplicity of aa-genprof and I really can see myself generating AppArmor profiles on my own.

A fully confined SELinux distribution might be more secure than a fully confined AppArmor system, because there is no service since it runs completely in the kernel and can only disabled by a kernel parameter, which makes it harder for attackers to get rid of it. AppArmor won’t survice systemctl stop apparmor, but I’ll accept the risk, because more possible confinement on the desktop means more security.

AppArmor is mostly backed by Canonical and OpenSUSE, while SELinux is obviously backed by Red Hat and the NSA. I don’t believe that the NSA baked backdoors into SELinux, but I think it’s worth mentioning it.

I personally like the idea of the AppArmor.d project and aa-genprof, which is why my next GNU/Linux distribution will be an AppArmor enabled distribution.

Further reads and talks regarding MAC

See also