Confine an application
In this guide, you will learn how to confine an application's behavior with an AppArmor profile and how to debug and refine that profile.
Prerequisites
- Install
apparmor-utilswhich provides the AppArmor CLI tools used in this guide. - Install Dino, a desktop application you will confine in this tutorial. If you are on Ubuntu, make sure to install a deb package and not a snap package. snap packages are shipped with AppArmor profiles by default.
sudo apt update
sudo apt install apparmor apparmor-utils dino-im
sudo zypper install apparmor-parser apparmor-utils dino
sudo pacman -S apparmor dino
sudo flatpak install flathub im.dino.Dino
AppArmor's security model
Before creating your first profile, keep AppArmor's core logic in mind: it allows or denies a process access to specific paths as well as various operations such as network permissions, Linux capabilities, signal and ptrace mediation.
Each permission entry is a rule. Rules are grouped in profiles, and a profile is applied to a process based on the executable path.
Check available profiles
In practice, profiles are simple text files usually stored in /etc/apparmor.d/. Profiles are usually created per application, which means each application can have its own specific permissions, allowing for great flexibility.
Check what profiles are available on your system:
sudo ls /etc/apparmor.d
Most filenames either match an application name (for example, firefox or thunderbird) or a dotted binary path (for example, usr.sbin.cupsd). Historically, AppArmor profiles commonly mirrored binary paths with / replaced by ., while for new profiles, the convention is using the application name.
You can also check which of these profiles are currently loaded in the kernel:
sudo cat /sys/kernel/security/apparmor/profiles
Create a profile for Dino
Now create a new profile for Dino.
First, create a plain text file in /etc/apparmor.d/ for the profile:
sudo touch /etc/apparmor.d/dino-im
Open this file with your preferred text editor.
To ensure the profile is attached to the correct executable, use the full absolute path to Dino's binary. The exact path may vary by distribution, so verify it on your system first (for example with command -v dino-im or command -v dino). This tutorial uses a named profile declaration with common executable naming variants:
profile dino-im /usr/bin/dino{,-im} {
}
profile dino-im /usr/bin/dino{,-im}defines a profile nameddino-imand attaches it to the executable path.{,-im}is a brace expression that expands/usr/bin/dino{,-im}to two alternatives:/usr/bin/dinoand/usr/bin/dino-im.
The next step is to add reusable policy building blocks.
Add reusable profile parts
Before adding app-specific rules, include commonly used policy building blocks: tunables and abstractions.
Tunables
Many Linux systems share common directory layouts, but paths can vary across distributions and enterprise environments. For example, home directories are often under /home/, but may differ in centrally managed setups.
To make profiles portable, common paths are defined as variables in tunables. You can create custom tunables, but starting with the shipped defaults is usually best.
For example, open the global tunable, which includes the default tunable files that define common path variables.
sudo cat /etc/apparmor.d/tunables/global
In the output, you will see that it loads variables for common system paths such as the home directory.
Now check the tunable that defines your home directory:
sudo cat /etc/apparmor.d/tunables/home
In the output, you will see variables such as @{HOMEDIRS} and @{HOME}, which are used to make profiles portable across systems with different home-directory layouts.
Include the global tunable in the profile:
#include <tunables/global>
profile dino-im /usr/bin/dino{,-im} {
}
Note that while #include lines look like comments, they are processed as directives, so place them near the top of the profiles so that all the reusable variables are loaded into your profile first.
Abstractions
While tunables let you reuse paths, abstractions let you reuse rules. They reduce duplication and make profiles easier to maintain.
As with tunables, you can define your own abstractions or use ones shipped by your distribution that are stored in /etc/apparmor.d/abstractions:
sudo ls /etc/apparmor.d/abstractions
The available set of abstractions also depends on your distribution. For example, systems with GNOME provide a gnome abstraction with rules for common desktop resources such as fonts or theme configuration.
These defaults are maintained as general-purpose policy and are a good starting point. You can always override them when your application has specific requirements.
For this tutorial, start with three abstractions:
-
base: baseline rules commonly needed by most Linux applications (for example locale and basic runtime resources). -
nameservice: rules related to hostname resolution and name service behavior. -
gnome: rules for GNOME desktop resources (fonts, themes, and so on).
If you open any of these abstractions, you will notice that in some cases the list of rules is extremely long and confusing. You do not need to understand every resource listed in them for now. Default abstractions provide reasonable security configurations for most applications.
Add these default abstractions to the Dino profile:
#include <tunables/global>
profile dino-im /usr/bin/dino{,-im} {
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/gnome>
}
Important
Notice how these #include statements are inside the curly braces: abstractions contain rules, and rules belong in the profile body.
The profile skeleton is now ready. It is a valid profile, but very restrictive. If you enforce it now, Dino will likely fail to start. The next section demonstrates why.
Put the profile in complain mode
AppArmor profiles are commonly used in two modes: complain and enforce:
-
In complain mode, AppArmor does not block operations, but it logs accesses that would be denied. This is especially useful during profile development.
-
In enforce mode, AppArmor actively denies operations that are not allowed by the profile.
Unconfined state
You might have noticed that some profiles in /sys/kernel/security/apparmor/profiles have (Unconfined) which means processes using this profile are not confined by an enforcing AppArmor profile.
If a profile is too restrictive and doesn't let the application access some essential resources, the application won't be able to function properly.
The mode can be set in the profile file or forced at load time by parser flags. Let's do it manually first to see how it works.
Add a profile flag manually
Edit the profile header and add a complain flag:
profile dino-im /usr/bin/dino{,-im} flags=(complain) {
}
The mode of this profile is declared but it hasn't been loaded yet.
Check the list of profiles and you will see that no dino-im profile entry appears yet:
sudo cat /sys/kernel/security/apparmor/profiles | grep dino-im
Now load the profile with apparmor_parser and check the profile again:
sudo apparmor_parser -r /etc/apparmor.d/dino-im
sudo cat /sys/kernel/security/apparmor/profiles | grep dino-im
You will see the profile listed with (complain).
For more details about parser options, see apparmor_parser.
Add a profile flag with apparmor_parser
In practice, instead of manually editing the files to change the modes, you can use apparmor_parser to do that.
Remove flags=(complain) from the profile file and save the file.
Load the profile with apparmor_parser:
sudo apparmor_parser -r -C /etc/apparmor.d/dino-im
sudo cat /sys/kernel/security/apparmor/profiles | grep dino-im
- The
-rflag means that the currently loaded profile in the kernel will be replaced by the updated profile - The
-Cflag forces the loaded profile into complain mode.
For full command syntax and all available options, see apparmor_parser.
Once you have experimented with apparmor_parser, make sure that the profile is in complain mode. Dino should start normally. Use it as you usually would: resize the window, sign in, join servers, and send messages. In the meantime, AppArmor logs the resources Dino tries to access.
Monitor the logs
Rules defined in the profile are enforced in the kernel, so when a confined application accesses a resource, the kernel records an AppArmor audit event. Depending on your distribution and logging setup, these events are then exposed through various tools such as:
dmesg -w: displays kernel messages in real time.journalctl -k -f: displays kernel audit events via systemd journal.aa-notify: displays recent AppArmor events in a concise format.aa-logprof: helps you review logged denials and interactively update profiles.
Let's use journalctl to follow system logs for Dino. Keep Dino running, open a new terminal, and filter for dino:
sudo journalctl -fx | grep "dino"
As you exercise features, watch the journalctl output. You will see entries similar to this one:
Jan 28 11:38:45 localhost.localdomain kernel: audit: type=1400 audit(1769596725.729:2389): apparmor="ALLOWED" operation="file_lock" class="file" profile="dino-im"" name="/home/username/.local/share/dino/dino.db-shm" pid=6287 comm="dino-im" requested_mask="k" denied_mask="k" fsuid=1000 ouid=1000
To interpret these entries, pay attention to profile= which indicates the binary path of the application (dino-im in our case) and name= which indicates the resource path the application is trying to access (.local/share/dino/dino.db-shm in our case). requested_mask="k" indicates a file-lock operation.
In the profile below, we allow Dino to read, write, and lock files under owner @{HOME}/.local/share/dino/** rwk,. Files in .local/share/dino store account data, media, and attachments, so these accesses are expected.
Add Dino-specific rules
Dino is a GNOME application that needs to store various data during runtime such as window size, user preferences or attachments received in the chat. We must give it permissions to access these resources.
Let's add a few rules:
- Allow Dino to read files belonging to the user who started it
- Allow Dino to map its own executable so it can start and restart
File rule syntax is path/to/file permissions,. In this tutorial, these permissions are used:
r= readw= writek= file lockm= memory map executable files
The profile can then look like this:
#include <tunables/global>
profile dino-im /usr/bin/dino{,-im} {
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/gnome>
owner @{HOME}/** r,
/usr/bin/dino{,-im} mr,
# Rules for reading/writing the database and attachments
owner @{HOME}/.local/share/dino/** rwk,
}
owner @{HOME}/** r,: allow Dino to read files in the current user's home directory/usr/bin/dino{,-im} mr,: allow Dino to read and memory-map its own executable, which is needed for normal startup and restart behavior.owner @{HOME}/.local/share/dino/** rwk,: allow Dino to read, write, and file-lock access to Dino's app data directory for storing temporary data such as windows size and position, theme, account preferences, attachments in the chat, and so on
Keep adding rules for your application's profile as you test it. A good practice is to group rules by purpose (runtime data, network, desktop integration, and so on).
In most cases, profile development follows this loop: 1. Put the profile in complain mode. 2. Exercise the application features you care about. 3. Review logs and identify legitimate accesses. 4. Add or tighten rules. 5. Reload the profile. 6. Repeat until the application is denied only operations you do not want it to perform.
After testing in complain mode, a realistic basic working profile might look like this:
#include <tunables/global>
profile dino-im /usr/bin/dino{,-im} {
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/gnome>
#include <abstractions/ssl_certs>
# Dino is a desktop client and needs network access.
network,
/usr/bin/dino{,-im} mr,
# Read user-owned files
owner @{HOME}/** r,
# Runtime data (database, attachments, lock files)
owner @{HOME}/.local/share/dino/** rwk,
# User config and cache used by GTK/desktop integration.
owner @{HOME}/.config/dino/** rwk,
owner @{HOME}/.cache/dino/** rwk,
# dconf backend used by GNOME apps for settings.
owner /run/user/*/dconf/user rwk,
}
This is an example of a profile. Exact rules will vary by distribution, desktop environment, and which Dino features you test.
Put the profile in enforce mode
Once you have configured and tested the profile, switch it to enforce mode so AppArmor denies disallowed operations:
sudo apparmor_parser -r /etc/apparmor.d/dino-im
If you try to launch Dino application now, you will likely get multiple error messages such as:
(process:57600): dconf-CRITICAL **: 17:13:48.808: unable to create file '/run/user/1506215667/dconf/user': Permission denied. dconf will not work properly.
Ship your profile
If you are developing a profile for an application you maintain, or for an application that is not covered by default but can help other users, consider submitting it to the AppArmor repository. The profile can then be maintained by the community and shipped in multiple distributions.
Learn other AppArmor tools
This tutorial uses apparmor_parser directly so you can see exactly how profiles are loaded, replaced, and switched between modes. apparmor-utils provides various tools for these common operations that can be used on a day-to-day-basis, such as:
aa-complain: switch the profile to complain mode.aa-enforce: switch the profile to enforce mode.aa-status: show AppArmor status and loaded profiles.