Learn AppArmor - Writing Basic Profiles
Episode 4 of 23

Learn AppArmor - Writing Basic Profiles

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.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

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.

Basic Profile Structure

An AppArmor profile is a text file with a simple structure. The most basic syntax:

LinuxBasic profile structure
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:

LinuxA simple profile for /usr/bin/cat
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:

  1. The profile name follows the executable path: /usr/bin/cat.
  2. The profile attaches to the executable itself — the /usr/bin/cat process must be able to read its own file, hence the line /usr/bin/cat r.
  3. The profile lists the libraries it needslibc.so.6 is allowed with mr access (read and mmap) because m is crucial for loading libraries, as we discussed in episode 2.
  4. The line 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.

File Rules: r, w, m, k

The four basic file accesses you must understand (from episode 2):

AccessMeaningWhen it's needed
rRead a fileReading config, logs, data
wWrite a fileWriting logs, cache, lock files
mMemory-map (mmap)Loading .so libraries into memory
kFile lockLocking 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:

LinuxFile access combinations
/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.

Path Globbing

The real world is rarely as simple as one file. To allow access to many files with a single pattern, AppArmor provides globs:

PatternMeaningExample 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:

LinuxPath globbing examples
/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.

Deny Rules

Sometimes the safest step isn't to allow, but to explicitly forbid. Deny rules are written with the deny keyword:

LinuxDeny rule examples
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.

Includes & Abstractions

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:

List the available abstractions
ls /etc/apparmor.d/abstractions

A Complete One-File Profile

Let's combine everything into one complete profile for nginx. Note this is a minimal profile for demonstration — a production version would be longer:

LinuxA minimal complete profile for nginx
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:

Validate then load the profile
sudo apparmor_parser -Q /etc/apparmor.d/usr.sbin.nginx
sudo apparmor_parser -a /etc/apparmor.d/usr.sbin.nginx
sudo aa-status | grep nginx

If -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.

Common Pitfalls

  1. Forgetting access to the profile's own file. The process must be able to read its executable — without /usr/sbin/nginx mr, the application can't run at all.
  2. Forgetting libraries (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.
  3. Overly broad globs. /etc/** rw nearly erases the value of protection. Start narrow, expand only when there's a legitimate denial.
  4. Missing trailing commas. Every rule inside the curly braces must end with a comma — a missing comma makes apparmor_parser -Q fail.
  5. Going straight to enforce without complain. For your first profile, run it in complain mode (episode 3) and watch the logs before promoting it to enforce.

Conclusion

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:

  • A profile attaches to the executable path and lists all allowed accesses inside the curly braces.
  • r read, w write, m mmap, k lock — understand when each is needed.
  • Globs shorten your writing: * one segment, ** everything, {a,b} alternatives.
  • Deny wins over allow — useful for closing holes in broad patterns.
  • Abstractions like <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!

Learn AppArmor - Writing Basic Profiles | Learn AppArmor