Confining Linux Services with AppArmor Profiles
Learn to write, test, and enforce AppArmor profiles that confine Linux services using aa-genprof and audit logs, with AI help and a human in the loop.
- #linux
- #apparmor
- #security
- #hardening
The first time I shipped a custom AppArmor profile to production, I broke a logging daemon for about forty minutes because I forgot that it wrote PID files into a directory I had never thought to allow. Nobody told me. The service just silently failed to start, the audit log filled with DENIED lines I did not yet know how to read, and I spent the afternoon learning that “least privilege” and “least understood” are very close neighbors. AppArmor is one of the highest-leverage hardening tools on a Linux box, but it punishes guesswork. This is a walkthrough of how I now write, test, and enforce profiles, where AI fits into that loop, and where it absolutely does not.
Why AppArmor Earns Its Keep
AppArmor is a mandatory access control system that confines individual programs to a declared set of capabilities and file paths. Unlike SELinux’s label-everything model, AppArmor is path-based, which makes profiles readable by humans and easy to reason about. A profile says, in effect, “this binary may read these files, write those, use these capabilities, and nothing else.” If the program is compromised, the blast radius is whatever you wrote down, not the whole filesystem.
Start by seeing what is already loaded:
sudo apparmor_status
That prints how many profiles are loaded, which are in enforce mode, which are in complain (audit-only) mode, and which processes have profiles attached. On a stock Ubuntu box you will already see profiles for things like tcpdump, man, and several MariaDB and CUPS helpers. The goal of a custom profile is to add your own service to that enforce list without breaking it.
If you maintain a fleet of these and want to organize the patterns you keep reaching for, a saved set of starting points in the Prompt Workspace beats retyping the same scaffolding every time.
Generating a First Draft with aa-genprof
You rarely write a profile from a blank file. The aa-genprof tool runs your service while watching what it actually does, then walks you through each access interactively.
sudo aa-genprof /usr/local/bin/my-service
Leave that running, then in another terminal exercise the service the way production would: start it, hit its endpoints, rotate its logs, trigger its cron path. Back in the aa-genprof window you press S to scan the logs, and it presents each observed access. For files it offers Allow, Deny, Glob (to generalize /var/log/my-service/2026-06-16.log into /var/log/my-service/*.log), and Glob with Extension. For capabilities it simply asks allow or deny. When you finish, it writes a profile into /etc/apparmor.d/ named after the binary path with slashes replaced by dots, for example usr.local.bin.my-service.
Pro Tip: Run aa-genprof on a staging box that mirrors production, never on the production host itself. The interactive scan only captures what you exercise, so an incomplete test run produces a profile that will deny real traffic the moment it ships.
This is exactly the kind of work where an AI assistant behaves like a fast junior engineer. Paste the generated profile into a tool like Claude or ChatGPT and ask it to explain each rule, flag anything overly broad, and suggest globs you missed. It is genuinely good at spotting that you allowed /etc/** r when you only needed one config file. But it is guessing from text, not from your running system, so you verify every suggestion against the actual audit log before you trust it.
Reading the Profile Syntax
A minimal profile is more approachable than it looks:
#include <tunables/global>
/usr/local/bin/my-service {
#include <abstractions/base>
#include <abstractions/nameservice>
capability net_bind_service,
capability setuid,
network inet stream,
/usr/local/bin/my-service mr,
/etc/my-service/config.yaml r,
/var/log/my-service/*.log rw,
/var/lib/my-service/** rwk,
/run/my-service.pid rw,
owner /tmp/my-service-* rw,
}
The permission letters matter: r read, w write, m memory map executable, k lock, x execute. The ** glob matches across directory boundaries; a single * stays within one directory. The owner keyword restricts a rule to files owned by the process’s user. The #include lines pull in abstractions, which are reusable bundles shipped in /etc/apparmor.d/abstractions/ for common needs like DNS resolution or TLS certificate access. Reaching for an abstraction is almost always better than hand-listing twenty files.
Testing in Complain Mode First
Never jump straight to enforcement. Put the profile in complain mode, where violations are logged but allowed:
sudo aa-complain /usr/local/bin/my-service
Now run the service through a full production-like cycle. Complain mode lets the service work normally while recording every access it makes that the profile does not yet permit. After a representative run, the audit log holds the gaps you need to close.
If you want to merge two drafts, say a hand-written baseline and a freshly generated one, aa-mergeprof reconciles them rule by rule:
sudo aa-mergeprof /etc/apparmor.d/usr.local.bin.my-service ./generated.profile
Parsing the Audit Log
The audit log is where the real work happens, and reading DENIED lines is the skill that separates a working profile from a 40-minute outage. The entries land in a few places depending on whether auditd is running:
# Live kernel ring buffer
sudo dmesg | grep -i apparmor
# Systemd journal, scoped to the audit tag
sudo journalctl -k | grep apparmor="DENIED"
# Structured search when auditd is installed
sudo ausearch -m AVC -ts recent | grep my-service
A denial line looks roughly like this:
apparmor="DENIED" operation="open" profile="/usr/local/bin/my-service" name="/var/lib/my-service/cache.db" pid=4821 comm="my-service" requested_mask="wk" denied_mask="wk" fsuid=1001
Read it field by field: the operation, the name of the resource, and the requested_mask tell you exactly what rule to add. Here the service wanted wk (write plus lock) on cache.db, which the ** rule under /var/lib/my-service/ already covers if you set it to rwk. This is another spot where AI shines as a junior pair: paste a wall of denial lines into Gemma or your editor’s assistant in Cursor and ask it to group them by path and propose the minimal rules. It will draft a tidy block in seconds. You still confirm each mask by hand, because an assistant that over-grants rwk everywhere quietly defeats the entire point of confinement.
Pro Tip: AI is a fast junior engineer, not a trusted operator. Let it draft and explain, keep a human in the loop on every rule, and never hand it production credentials or shell access to apply changes directly. It works on the text you paste, not on your live audit stream.
Converging with aa-logprof
Rather than editing the profile by hand for every denial, let aa-logprof read the audit log and walk you through the new accesses, the same interactive flow as aa-genprof but driven by what complain mode just recorded:
sudo aa-logprof
It scans for DENIED and ALLOWED audit events, proposes rules with the same allow/deny/glob choices, and updates the profile in place. Run it, exercise the service again, run it again. Two or three iterations usually drives the denials to zero. This loop is the heart of AppArmor profile development.
Enforcing and Reloading
Once complain mode produces a clean run, switch to enforcement:
sudo aa-enforce /usr/local/bin/my-service
After any manual edit to a profile file, reload it so the kernel picks up the change:
sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.my-service
Confirm the mode with sudo apparmor_status and watch the journal for a few minutes under real load. Wire that watch into whatever you already use for monitoring alerts so a fresh DENIED after deploy pages you instead of silently failing. When you fold a new profile into a release, a quick pass through code review catches the broad ** rule someone added at 5pm on a Friday.
Conclusion
A good AppArmor profile is a contract: this service does exactly these things and no more. The path to one is unglamorous but reliable: generate a draft, run in complain mode, read the audit log, converge with aa-logprof, then enforce. Lean on AI to explain syntax, group denials, and spot over-broad rules, the way you would lean on a sharp junior engineer who is fast but unproven. Keep a human approving every rule, never give the model your production keys, and let the audit log, not the assistant, be the final word. If you want starting points for the prompts I use during this loop, the Linux admins category, the prompt library, and the downloadable prompt packs are where I keep them.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.