The practice of writing your first AppArmor profile: the profile syntax with rules inside curly braces, the r w m k file rules, path globbing for directory patterns, deny rules for explicit access bans, and the use of the includes and abstractions shipped by your distro.

In episode 3 we learned to load, unload, and change profile modes — but all of that was done against existing profiles. Now it's time for the most creative part: writing a profile from scratch. This is the core skill of this series; in episode 5 you'll build the same profile with the help of automated tools, and in later episodes you'll extend it to real services.
Imagine writing an SOP (Standard Operating Procedure) for a machine. That SOP must be specific: who may enter which room, which documents may be read, and what is strictly forbidden. Too loose and the SOP is useless; too strict and the machine stops working. Writing an AppArmor profile is the same art.
An AppArmor profile is a text file with a simple structure. The most basic syntax:
profile <nama-profil> {
<aturan akses>
}The profile <name> line names the profile — usually taken from the path of the executable being protected. Inside the curly braces, we write the access rules. Let's look at a real, very simple profile:
profile /usr/bin/cat {
/usr/bin/cat r,
/etc/ld.so.cache r,
/lib/x86_64-linux-gnu/libc.so.6 mr,
/etc/localtime r,
owner /home/*/ r,
}Notice a few things from the example above:
/usr/bin/cat./usr/bin/cat process must be able to read its own file, hence the line /usr/bin/cat r.libc.so.6 is allowed with mr access (read and mmap) because m is crucial for loading libraries, as we discussed in episode 2.owner /home/*/ r allows reading files owned by the same user in the home directory — the owner pattern restricts the rule to files owned by the process owner.The four basic file accesses you must understand (from episode 2):
| Access | Meaning | When it's needed |
|---|---|---|
r | Read a file | Reading config, logs, data |
w | Write a file | Writing logs, cache, lock files |
m | Memory-map (mmap) | Loading .so libraries into memory |
k | File lock | Locking files for synchronization |
In a single rule, multiple accesses can be combined: mr means read and mmap allowed, rw means read and write. Example rule lines:
/etc/apparmor.d/nginx r,
/var/log/nginx/access.log rw,
/lib/x86_64-linux-gnu/libc.so.6 mr,
/var/run/nginx.pid rwk,The last line uses rwk — read, write, and lock — because pid files are often locked while the process writes them. Understanding these combinations is what separates a working profile from one that makes applications error out.
The real world is rarely as simple as one file. To allow access to many files with a single pattern, AppArmor provides globs:
| Pattern | Meaning | Example match |
|---|---|---|
* | Any path segment (not /) | /usr/bin/* matches /usr/bin/cat |
** | Any path segments including / | /etc/** matches /etc/nginx/nginx.conf |
? | A single character | /usr/bin/ngin? matches /usr/bin/nginx |
{a,b} | One of the listed alternatives | /etc/{hosts,resolv.conf} |
Examples of glob usage in a profile:
/etc/nginx/** r,
/var/log/nginx/*.log rw,
/usr/share/nginx/** r,
{var,etc}/** r,The {var,etc}/** pattern in the last line allows read access to everything under /var and /etc — a pattern often seen in profiles for long-running daemons. Be careful: an overly broad glob like /etc/** rw basically removes protection, so always keep it as minimal as possible.
Sometimes the safest step isn't to allow, but to explicitly forbid. Deny rules are written with the deny keyword:
profile /usr/bin/git {
/usr/bin/git r,
deny /root/** w,
deny /etc/shadow r,
deny /home/*/.ssh/** r,
}Why do you need deny when the default principle is closed unless allowed? Because there's a gap: you might start with a broad rule like /home/*/** r, then want to close off part of it — for example ~/.ssh. The deny rule acts as a fence inside the fence: it narrows access previously allowed by a wider pattern, and it always wins over allow rules.
Warning
AppArmor's security principle remains "closed unless allowed" — deny is not a substitute for minimalism. But deny is very useful for closing specific holes in broad patterns, or for stating intent ("this folder must never be accessed") so it reads clearly to the rest of your team.
Writing library paths manually in every profile is tedious and error-prone. The solution: includes. The /etc/apparmor.d/tunables directory holds variables and configuration values, and the /etc/apparmor.d/abstractions directory holds ready-made rule blocks. Three includes that appear in almost every daemon profile:
#include <tunables/global> loads global variables like common path definitions.#include <abstractions/base> provides the base rules every process needs: standard library access, /etc/ld.so.cache, and the like.#include <abstractions/openssl> adds the access needed to secure TLS connections.Here's a more realistic profile: core rules written by hand, and boilerplate pulled from abstractions. If you're still new to writing profiles, abstractions are the fastest way to learn — open the abstraction files and study their patterns:
ls /etc/apparmor.d/abstractionsLet's combine everything into one complete profile for nginx. Note this is a minimal profile for demonstration — a production version would be longer:
profile /usr/sbin/nginx {
#include <tunables/global>
#include <abstractions/base>
#include <abstractions/openssl>
capability net_bind_service,
capability setuid,
capability setgid,
network inet tcp,
/usr/sbin/nginx mr,
/etc/nginx/** r,
/var/log/nginx/** w,
/var/lib/nginx/** rw,
/var/run/nginx.pid rwk,
deny /etc/shadow r,
}To activate this profile, save it as /etc/apparmor.d/usr.sbin.nginx, then follow the flow we learned in episode 3:
sudo apparmor_parser -Q /etc/apparmor.d/usr.sbin.nginx
sudo apparmor_parser -a /etc/apparmor.d/usr.sbin.nginx
sudo aa-status | grep nginxIf -Q shows no errors and grep finds the nginx profile in the aa-status output, your profile loaded successfully. Don't forget to test the application: start the service and make sure there are no denials in the logs — if there are, adjust. In episode 5, the aa-genprof and aa-logprof tools will automate this adjustment process.
/usr/sbin/nginx mr, the application can't run at all.m). Applications that error with "cannot open shared object" usually have a library that isn't allowed to be mmap'd. Use the base abstraction to cover this case./etc/** rw nearly erases the value of protection. Start narrow, expand only when there's a legitimate denial.apparmor_parser -Q fail.In episode 4 we learned to write profiles from scratch: the profile <name> { ... } structure, the r w m k file rules and their combinations, path globbing with * ** ? and {a,b}, deny rules for explicit bans, and includes for tunables and abstractions to reuse ready-made rules.
The core takeaways:
r read, w write, m mmap, k lock — understand when each is needed.* one segment, ** everything, {a,b} alternatives.<abstractions/base> remove boilerplate; get into the habit of -Q before loading.In episode 5, we'll multiply your productivity: aa-genprof and aa-logprof — creating profiles interactively from a target executable, allowing or denying each request that appears, and updating profiles from denial logs. Keep your momentum, because with these two tools you'll never write a profile from a blank page again!