AppArmor profiles
Path-based confinement, complain to enforce.
Hire a plumber and you might hand them a key that opens the bathroom and the utility closet, and nothing else. Not the safe, not the bedrooms. AppArmor (Application Armor) hands out keys exactly like that, one per program. It is the Mandatory Access Control system that ships turned on by default on Debian and Ubuntu. Mandatory Access Control (MAC) means the rules are written by the system and enforced by the kernel, the core of the operating system that sits between programs and the hardware, no matter which user runs the program, root included. Ordinary Linux permissions work the opposite way. They are discretionary (Discretionary Access Control, or DAC): the owner of a file decides who may read it, and a running program inherits all the power of the user behind it. AppArmor adds a second layer the user cannot wave away. For each confined program it keeps a profile: a written list of the exact files that program may touch, the capabilities it may pick up (small, named slices of root's power), and the network operations it may make. A profile for the nginx web server says it may read the web root, bind its ports, and little else. So when an attacker takes over nginx, they inherit only what the profile allows, not the run of the whole machine.
Your first question on any host is what is confined, and how tightly. aa-status answers it.
Read that top to bottom. Profiles in enforce mode are live keys: anything the profile does not permit, the kernel denies. Profiles in complain mode are watched but not blocked (more on that shortly). Kill mode goes one step further and kills a program the instant it does something off-list. Unconfined mode is the strange one: the profile is loaded but set to allow everything, so it is attached to the program and yet holds it to nothing at all. For your purposes that reads as no protection, and you almost never want to see it. The profiles themselves live as plain text in /etc/apparmor.d/, one file per program, named after the binary with the slashes turned into dots, so /usr/local/bin/myapp becomes usr.local.bin.myapp. The bottom half of the report pairs profiles with the running processes, which is how you confirm a service you care about is genuinely confined right now, not merely defined on disk.
What a profile actually says
A profile is an allowlist, the guest list on a club door. If an action is on the list it goes through; if it is not, it is turned away. The profile spells out what the program may do and denies everything else by default, so you never have to predict every bad action, only name the handful of good ones. Each rule is a path plus a set of access modes: r to read, w to write, a to append only, m to memory-map a file as executable (which a program needs in order to load its own code and its shared libraries), k to lock a file, and l to create links. There are also execute-transition modes that decide what happens when the confined program launches another binary, but the file rules are where you spend most of your time.
#include <tunables/global>profile myapp /usr/local/bin/myapp {#include <abstractions/base>#include <abstractions/nameservice>capability net_bind_service, # may bind a port below 1024network inet stream, # may open IPv4 TCP socketsnetwork inet6 stream, # ... and IPv6/usr/local/bin/myapp mr, # map + read its own binary/etc/myapp/** r, # read its config tree/var/lib/myapp/** rw, # read/write its data dir/var/log/myapp/*.log w, # write its log filesdeny /etc/shadow r, # never read the password hashesdeny /home/** r, # never read user data}
Two lines here do a lot of quiet work. #include <abstractions/base> pulls in the baseline accesses almost every program needs (shared libraries, /dev/null, locale data), so you are not re-listing them in every profile. capability net_bind_service grants one specific power: the ability to bind a port below 1024. Think of the root account's authority as a single master key; Linux capabilities are that key sawn into a few dozen small ones, and AppArmor hands this program exactly one of them. The two network lines are narrow in the same way. They let myapp open TCP (Transmission Control Protocol) connections over IPv4 and IPv6, the old and new versions of the internet's addressing scheme, and nothing else, so it cannot open the kind of raw socket you would use to quietly sniff traffic. The two deny lines are belt-and-suspenders. Reads are already denied by default, but writing deny /etc/shadow r and deny /home/** r states the intent out loud and blocks those reads even if a later rule or an included abstraction would otherwise have permitted them.
Path, not label
AppArmor matches on the path, the file's address on disk, not on a tag stuck to the file itself. That is what makes a profile easy to read: every line names a place you can walk over to and look at. It is the big practical difference from SELinux (Security-Enhanced Linux, the other main Mandatory Access Control system on Linux), where the rules are written against labels attached to every file and process, so to see why something was denied you first have to go read the labels. The path model also sets the boundary you have to think about. A rule grants access to a path, so if the same bytes are reachable by another path (a symlink, a bind mount, a second hard link), the rule that names one path does not automatically cover the other. AppArmor resolves symlinks down to the real path and matches on that, which is usually what you want, but it means you write rules against where files actually live. Keep confined binaries where their profile expects them, and be careful about granting write access to a directory an attacker could fill with symlinks.
Complain first, then enforce
You would not fit a new lock to your front door and hand back the only key without checking it turns. AppArmor builds the same caution in, and calls it complain mode. In complain mode the profile logs every access the program makes and blocks none of them, so you can watch a real workload, confirm the profile covers everything legitimate, and only then switch to blocking. This is the same tune-then-enforce discipline you find in SELinux and in seccomp (secure computing mode, the kernel feature that filters which system calls a program is allowed to make). You never flip an unproven profile straight to enforce on a production service, because the first legitimate access it forgot to allow becomes an outage.
Now run the program through its real work: hit every endpoint, run every job, touch every config it reads on a normal day. Each access lands in the log. aa-logprof then reads that log and walks you through the new accesses one at a time, proposing a rule for each and letting you accept, generalize, or reject it.
Press A to allow the rule as shown, or G to turn a specific filename into a wildcard so the rule covers a whole directory instead of one file, which is how you avoid a profile that breaks the moment a temp file gets a new name. Deny records that the access should never be permitted. When you finish, aa-logprof writes the updated rules back into the profile. With the profile now covering the real workload, switch it to blocking.
Watch the denials
Enforcement is only half the value. The other half is that every block is written down, so a denial is both a stopped action and a lead worth reading. On a modern systemd host (systemd is the init system that starts and supervises services on most Linux distributions today) the AppArmor kernel messages land in the journal, systemd's central log store, as audit records. Pull them with journalctl.
Read one line like a sentence. profile="myapp" is which confined program tripped, operation="open" and name="/etc/shadow" are what it tried to touch, requested_mask="r" is that it wanted to read, and apparmor="DENIED" is the kernel refusing. Those two lines tell a story: something running as myapp reached for the password hashes and for a user's SSH private key, and the profile stopped both cold. That is the exact shape of a compromised process probing for secrets, and the profile turned it into an alert instead of a breach. One caution for the tuning phase: in complain mode these same lines read apparmor="ALLOWED", because complain audits without blocking. Seeing ALLOWED is your reminder that the profile is not protecting anything yet.
Reload and verify
When you edit a profile by hand, the change on disk does nothing until you load it into the kernel. apparmor_parser -r compiles and reloads a single profile in place. At boot the apparmor systemd service loads everything in /etc/apparmor.d/ for you, so your edits stick across reboots. To prove the live state rather than trust it, read the kernel's own list of profiles straight from securityfs, a small in-memory filesystem the kernel uses to expose the state of its security modules. It prints each profile with its current mode in parentheses.
myapp was DENIED read on /etc/shadow and on /home/deploy/.ssh/id_ed25519. The app has never legitimately needed either file. What is the right reading?Wire aa-status into whatever watches your fleet, and alert when an internet-facing profile drops out of enforce, whether someone set it to complain to debug a problem and forgot, or a deploy shipped a new build the old profile no longer matches. A web server, a mail daemon, or a browser running unconfined is exactly the gap these profiles exist to close, so the profile you most need in enforce is the one on whatever is listening to the internet.
Try this
Work through “Reload and verify” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: complain does not protect, and disabling throws it away. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.