Writing AppArmor Profiles
Our AppArmor explainer covers what it is. This covers writing a profile for something that does not have one.
Path-based confinement
AppArmor rules match file paths:
/etc/myapp/config.yml r,
/var/lib/myapp/** rw,
/usr/bin/myapp mr,
That readability is the whole appeal. A person can look at a profile and understand what the program is permitted to do, which is rarely true of an SELinux policy.
The structural weakness follows from the same property: a different path to the same file is a different rule. A hard link, a bind mount, or a symlink can reach a file by a path the profile does not cover. SELinux labels inodes instead, which survives renaming and linking, and is correspondingly harder to learn.
Both are real mandatory access control. AppArmor is the default on Ubuntu, Debian, and SUSE; SELinux on Fedora and RHEL.
Building a profile
sudo apt install apparmor-utils
sudo aa-genprof /usr/local/bin/myapp
aa-genprof starts a guided session. In another terminal, exercise the application thoroughly: start it, use every feature, trigger the error paths, restart it. Then return and let it scan the logs.
For each denial it asks whether to allow, and whether to use a specific path or a glob. The result is a profile in /etc/apparmor.d/.
The quality of the profile depends entirely on how completely you exercised the application. A feature you never triggered generates no log entry, produces no rule, and breaks in production.
Complain mode
Safer than generating blind, and the mode to spend real time in.
sudo aa-complain /usr/local/bin/myapp
sudo aa-status | head
Complain mode allows everything and logs what would have been denied. Run the application normally for a day or a week under realistic load, then harvest:
sudo aa-logprof
This reads the accumulated denials and updates the profile interactively. Repeat until a normal period produces no new denials, then enforce:
sudo aa-enforce /usr/local/bin/myapp
The discipline that matters: stay in complain mode longer than feels necessary. The failure mode is enforcing a profile built from an afternoon’s testing and discovering the monthly report job needs a path nobody exercised.
Writing one by hand
#include <tunables/global>
/usr/local/bin/myapp {
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/openssl>
capability net_bind_service,
network inet stream,
network inet6 stream,
/usr/local/bin/myapp mr,
/etc/myapp/ r,
/etc/myapp/** r,
/var/lib/myapp/ rw,
/var/lib/myapp/** rwk,
/var/log/myapp/*.log w,
/run/myapp.pid rw,
owner /tmp/myapp-* rw,
deny /etc/shadow rwklx,
deny /home/*/.ssh/** rwklx,
/bin/sh ix,
/usr/bin/convert Px,
}
The parts worth knowing:
Abstractions are reusable fragments. abstractions/base covers what essentially every program needs, which saves you rediscovering that it wants /dev/urandom.
Permission letters: r read, w write, a append, k lock, l link, m memory-map executable, x execute.
Execute modes are the subtle part:
ixinherit: the child runs under this same profilepxrequires a separate profile for the child, and fails if none existsPxthe same, plus environment scrubbinguxunconfined, which is an escape hatch and rarely correctcxtransitions to a child profile defined inside this one
Use Px over px. The environment scrubbing strips LD_PRELOAD and friends, which otherwise offer a route to influence the child.
owner restricts a rule to files owned by the running user, which is how you permit temporary files without permitting access to everyone else’s.
deny rules are absolute and cannot be overridden by a later allow. Explicit denies on /etc/shadow and SSH keys are cheap insurance against an over-broad glob written later.
Loading and checking
sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.myapp
sudo apparmor_parser -Q /etc/apparmor.d/usr.local.bin.myapp # syntax only
sudo aa-status
sudo systemctl restart myapp
Reloading applies to newly started processes. A process already running keeps the profile it started under, so restart the service to be sure what is enforced.
Reading denials
sudo journalctl -k | grep -i apparmor
sudo ausearch -m AVC -ts recent -i
sudo dmesg | grep DENIED
apparmor="DENIED" operation="open" profile="/usr/local/bin/myapp"
name="/etc/myapp/extra.conf" pid=4821 comm="myapp"
requested_mask="r" denied_mask="r" fsuid=1000
That tells you the profile, the path, and the permission. Adding /etc/myapp/extra.conf r, fixes it.
If auditd is running, AppArmor denials arrive as AVC records and ausearch is the better tool.
The common confusion: a program failing for an unrelated reason while a profile is loaded gets blamed on AppArmor. Check for an actual DENIED line before assuming.
Containers
Docker applies docker-default automatically. A custom profile is a useful extra layer:
sudo apparmor_parser -r /etc/apparmor.d/docker-myapp
docker run --security-opt apparmor=docker-myapp myimage
This complements rather than replaces the measures in our container security guide: dropping capabilities, read-only root, and a non-root user each address something AppArmor does not.
Where it fits
AppArmor confines a program’s access to the filesystem and network. It sits alongside rather than instead of:
- systemd hardening directives, which use namespaces and seccomp
- Capability dropping
- Running as an unprivileged user
There is meaningful overlap with systemd’s ProtectSystem and friends, and in many cases those are easier to apply and get you most of the way. AppArmor is the right tool when you need per-path granularity that systemd’s coarser directives cannot express, or when your distribution already ships profiles you want to extend.
Worth knowing about: the Canonical-funded work using AppArmor as a case study for C-to-Rust translation research, which is a sign of how central it is to Ubuntu’s security model.
Frequently Asked Questions
What is the difference between complain mode and enforce mode?
Complain mode allows everything and logs what would have been denied, which is how you discover what a program actually needs. Enforce mode blocks anything not permitted. The normal workflow is to run in complain mode under realistic use, build the profile from the logs, then switch to enforce.
How is AppArmor different from SELinux?
AppArmor confines by file path, so rules read like filesystem paths and are relatively easy to write. SELinux labels inodes and enforces on the labels, which survives renames and is more rigorous and considerably harder to learn. AppArmor is the default on Ubuntu and SUSE, SELinux on Fedora and RHEL.
Why does my profile break when a file is moved or a symlink is involved?
Because AppArmor matches on the path used to access a file. A hard link or a different path to the same inode is a different rule as far as AppArmor is concerned, which is the known structural weakness of path-based confinement and the main argument for SELinux.
What do the letters in an AppArmor rule mean?
r is read, w is write, a is append, x is execute, m is memory map executable, k is file locking, and l is link. Execute has variants including ix for inherit, px for a separate profile, and Px which also scrubs the environment, which is the safest choice.
Do I need to restart the application after changing a profile?
Reloading the profile with apparmor_parser applies to new processes immediately, and an already running process keeps the profile it started with. In practice you reload the profile and restart the service to be sure of what is applied.
Can AppArmor confine containers?
Yes, and Docker applies a default profile called docker-default to containers automatically. You can supply a custom profile with the security-opt flag, which is a useful additional layer alongside dropping capabilities and running read-only.