A stock Ubuntu 26.04 server with the AppArmor tools installed loads 202 profiles, yet only three running processes are actually confined by them. That gap is why this AppArmor cheat sheet exists. The commands that load, switch and debug profiles matter far more than the profile count, and most guides still show output from releases that no longer ship. Every line below was captured on lab VMs, and the headline numbers come from AppArmor 5.0.2 (tested November 2026 on Ubuntu 26.04).
We ran the same AppArmor commands on Ubuntu 26.04, Ubuntu 24.04 and Debian 13, and the three systems did not behave the same. The page starts with quick-reference tables for aa-status, aa-enforce, aa-complain, aa-disable, aa-genprof, aa-logprof and apparmor_parser. After that comes a real profile build, the denial log lines you will see in journalctl, dmesg and audit.log, and the fix for each error we hit.
AppArmor Cheat Sheet (Quick Reference)
These tables are the short form of every section below. The tools live in the apparmor-utils package on all three distros, and most of them sit in /usr/sbin, so run them with sudo. In a non-interactive SSH session on Debian 13, a regular user did not even have /usr/sbin in its PATH, so apparmor_parser --version returned “command not found” until we added it.
Check status and loaded profiles
| Command | What it does |
|---|---|
aa-enabled | Prints Yes, or why AppArmor is off |
sudo aa-status | Loaded profiles and confined processes by mode |
sudo aa-status --enabled | Silent check, exit code 0 when enabled |
sudo aa-status --profiled | Number of loaded profiles |
sudo aa-status --enforced | Number of profiles in enforce mode |
sudo aa-status --complaining | Number of profiles in complain mode |
sudo aa-status --pretty-json | Status as formatted JSON |
sudo aa-status --filter.profiles='.*tcpdump.*' | Show only profiles matching a regex |
ps -eo pid,label,comm | Profile label of every process |
sudo aa-unconfined | Network-facing processes with no profile |
sudo cat /sys/kernel/security/apparmor/profiles | Raw kernel list of loaded profiles |
Switch profile modes
| Command | What it does |
|---|---|
sudo aa-enforce /usr/bin/tcpdump | Enforce: block and log violations |
sudo aa-complain /usr/bin/tcpdump | Complain: allow and log violations |
sudo aa-audit /usr/bin/tcpdump | Add flags=(audit), log allowed access too |
sudo aa-audit -r /usr/bin/tcpdump | Remove the audit flag again |
sudo aa-disable /usr/bin/tcpdump | Unload profile, link it in disable/ |
sudo aa-enforce /etc/apparmor.d/usr.bin.tcpdump | Same, by profile file instead of binary |
sudo aa-exec -p tcpdump -- cat /etc/hostname | Run a command under a named profile (denied here) |
Load and test profiles with apparmor_parser
| Command | What it does |
|---|---|
sudo apparmor_parser -r FILE | Load or replace a profile (use after edits) |
sudo apparmor_parser -a FILE | Add a new profile, fails if already loaded |
sudo apparmor_parser -R FILE | Remove the profile from the kernel |
sudo apparmor_parser -C FILE | Load the profile in complain mode |
sudo apparmor_parser -Q FILE | Syntax check only, no kernel load |
sudo apparmor_parser -N FILE | Print the profile names in a file |
apparmor_parser --version | Userspace version |
Build and update profiles
| Command | What it does |
|---|---|
sudo aa-genprof /usr/local/bin/app | Interactive profile build from a live run |
sudo aa-autodep /usr/local/bin/app | Skeleton profile in complain mode |
sudo aa-logprof | Turn logged events into profile rules |
sudo aa-logprof -f /tmp/kern.log | Read events from a log file you choose |
sudo aa-remove-unknown -n | Dry run of unloading profiles with no file |
Read AppArmor denial logs
| Command | What it does |
|---|---|
sudo journalctl -k -g 'apparmor="DENIED"' | Denials in the kernel journal (no auditd) |
sudo dmesg | grep 'apparmor="DENIED"' | Denials in the ring buffer (no auditd) |
sudo grep 'apparmor="DENIED"' /var/log/audit/audit.log | Denials once auditd is running |
sudo ausearch -m AVC -ts recent --input-logs | Last 10 minutes of AVC records (Ubuntu) |
sudo aa-notify -s 1 -v | Denials from the last day, readable |
sudo aa-notify -s 1 -v -f /var/log/audit/audit.log | Same, forcing the audit log |
Service and boot control
| Command | What it does |
|---|---|
sudo systemctl reload apparmor | Reload every profile in /etc/apparmor.d |
sudo systemctl stop apparmor | Stops the unit, profiles stay loaded |
sudo aa-teardown | Unload profiles, then check the count |
apparmor=0 on the kernel command line | Disable AppArmor at boot |
sudo sysctl kernel.apparmor_restrict_unprivileged_userns | Ubuntu user namespace restriction |
Profile syntax quick reference
| Rule | Meaning |
|---|---|
include <abstractions/base> | Pull in a shared rule set |
/etc/myapp/** r, | Read every file below /etc/myapp |
/var/log/myapp/*.log w, | Write log files in one directory |
/var/lib/myapp/{data,cache}/ rw, | Alternation: two directories |
owner @{HOME}/.config/myapp/** rw, | Only files the process user owns |
deny /etc/shadow r, | Explicit deny, wins over allow |
/usr/bin/sed ix, | Run sed inside the same profile |
/usr/bin/curl Px, | Run curl under its own profile |
capability net_bind_service, | Bind ports below 1024 |
network inet stream, | IPv4 TCP sockets |
include if exists <local/myapp> | Site overrides that survive updates |
File permission letters
| Letter | Meaning |
|---|---|
r | Read |
w | Write (conflicts with a) |
a | Append |
m | mmap with PROT_EXEC (load as code) |
k | Lock |
l | Link |
ix | Execute, inherit the current profile |
px / Px | Execute under the program’s own profile |
cx / Cx | Execute under a child profile |
ux / Ux | Execute unconfined (avoid) |
Upper-case execute modes (Px, Cx, Ux) also scrub the environment through the loader’s secure-execution mode, so prefer them over the lower-case forms when a confined program starts another one. The rest of this page explains each table with the output our three test machines produced.
1. Check AppArmor Status and Loaded Profiles
AppArmor is enabled by default on Ubuntu and Debian, so the first job is to confirm it and count what it actually confines. Install the user-space tools first, because a minimal or cloud image ships the kernel module and the apparmor package but not aa-genprof, aa-logprof or the mode-switch commands:
sudo apt update
sudo apt install -y apparmor-utils apparmor-profiles apparmor-notify
The fastest check is aa-enabled, which needs no root and prints a single word. The counting options of aa-status are useful in scripts and monitoring checks because they print one number and nothing else:
aa-enabled
sudo aa-status --profiled
sudo aa-status --enforced
sudo aa-status --complaining
On the Ubuntu 26.04 VM the numbers came back as 202 loaded, 126 enforcing and 2 complaining, and the process label column shows which daemons are really inside a profile:

Read the count with care. Of the 202 profiles, 74 were in “unconfined” mode: those are the Ubuntu profiles for apps such as browsers and Discord that exist only to grant user namespace access, and they restrict nothing. Only rsyslogd and the two chronyd processes ran under an enforcing profile, while systemd, sshd, systemd-networkd and systemd-resolved showed as “not confined” in sudo aa-unconfined. If you are hardening a server, that list of unconfined network services is the real to-do list, and our Ubuntu 26.04 server hardening guide covers the other layers around it.
The same commands gave different totals on the other two machines, all measured with apparmor-utils and apparmor-profiles installed:
| aa-status count | Ubuntu 26.04 | Ubuntu 24.04 | Debian 13 |
|---|---|---|---|
| Profiles loaded | 202 | 140 | 127 |
| Enforce mode | 126 | 45 | 7 |
| Complain mode | 2 | 4 | 44 |
| Unconfined mode | 74 | 91 | 76 |
| Processes in enforce mode | 3 | 1 | 0 |
Debian 13 puts most of the extra profiles from apparmor-profiles in complain mode, and Ubuntu enforces far more of them. The Debian box still confined zero processes for a simpler reason: the cloud image runs no profiled daemon at all (rsyslog is not even installed), so 127 loaded profiles had nothing to attach to. For JSON output that a monitoring agent can parse, sudo aa-status --pretty-json returns a "version": "2" document with a profiles map of name to mode.
2. Switch Profiles Between Enforce, Complain and Disabled
A profile is in one of three useful states: enforce blocks and logs, complain allows and logs, and disabled is not loaded at all. Each mode command takes either the program path or the profile file, rewrites the flags in the file and reloads it in one step:
sudo aa-complain /usr/local/bin/bench-report
sudo aa-enforce /usr/local/bin/bench-report
Both print a single confirmation line per profile:
Setting /usr/local/bin/bench-report to complain mode.
Setting /usr/local/bin/bench-report to enforce mode.
Disabling is different, because aa-disable unloads the profile and drops a symlink into /etc/apparmor.d/disable/ so the profile also stays off after a reboot:
sudo aa-disable /usr/local/bin/bench-report
ls -l /etc/apparmor.d/disable/
The link points back at the profile file, which is untouched:
Disabling /usr/local/bin/bench-report.
total 0
lrwxrwxrwx 1 root root 42 Nov 4 05:31 usr.local.bin.bench-report -> /etc/apparmor.d/usr.local.bin.bench-report
Running aa-enforce on the same path removed that symlink on both Ubuntu releases. The kernel list then showed /usr/local/bin/bench-report (enforce) again, so there is no separate “enable” command to remember. The 2023 version of this page told readers to disable a profile by switching it to complain mode. That is wrong: complain mode still loads the profile and writes an ALLOWED line for every access it would have blocked.
Audit mode is the one to use when you need a record of allowed access, for example before tightening a profile. sudo aa-audit printed “Setting /usr/local/bin/bench-report to audit mode.” while the kernel list in /sys/kernel/security/apparmor/profiles still showed (complain). Audit behaves as a flag on top of the mode. On the stock tcpdump profile, aa-audit after aa-complain left flags=(audit, complain) on line 4. A following aa-enforce changed it to flags=(audit) on all three distros, with the kernel reporting tcpdump (enforce). sudo aa-audit -r /usr/bin/tcpdump removed the flag again (“Removing audit mode from /usr/bin/tcpdump.”) and left the profile enforcing. Mode commands on that profile also print “Warning: profile tcpdump represents multiple programs”, which is harmless; it appears because the profile has a short name and an attachment path.
3. Build a New Profile with aa-genprof
To test profile generation without risking a real service, we confined a small shell script that reads one file, writes two and optionally reads /etc/machine-id. Create it:
sudo vim /usr/local/bin/bench-report
Paste the script exactly as it ran in the lab:
#!/bin/sh
# demo app confined in the AppArmor cheat sheet lab
cat /etc/hostname
date > /var/tmp/bench-report.txt
cp /etc/os-release /var/tmp/os-release.bak
if [ "$1" = "--id" ]; then
cat /etc/machine-id
fi
Make it executable, then start the profiler. aa-genprof writes a skeleton profile in complain mode and waits for you to run the program in a second terminal:
sudo chmod 755 /usr/local/bin/bench-report
sudo aa-genprof /usr/local/bin/bench-report
In the second terminal, run the program as a normal user and exercise every code path you want the profile to allow. Then press S in the first terminal to scan the log, answer each prompt, save with S and finish with F. The Ubuntu 26.04 session looked like this:

That capture holds two changes that break older tutorials:
- The inherit key is now h: AppArmor 4.1 and later (Debian 13 and Ubuntu 26.04) show In(h)erit and use I for Ignore. Ubuntu 24.04 (AppArmor 4.0.1) still shows (I)nherit with no Ignore option on that prompt, so a muscle-memory I on the newer releases skips the rule instead of adding it.
- The binaries moved: Ubuntu 26.04 runs
catanddatefrom/usr/lib/cargo/bin/coreutils/andcpfrom/usr/bin/gnucp. A hand-written/usr/bin/cat mrix,rule never matches there.
Both point to the same habit: let genprof or logprof record the real paths on the release you deploy to, and copy profiles between releases only after a test run in complain mode.
After pressing h for the three executables and A for the file accesses, genprof wrote this profile on Ubuntu 26.04 and switched it to enforce mode:
sudo cat /etc/apparmor.d/usr.local.bin.bench-report
The abi <abi/5.0> line pins the policy feature set; Ubuntu 24.04 wrote abi/3.0 and Debian 13 wrote abi/4.0 for the same script:
# Last Modified: Wed Nov 4 05:29:17 2026
abi <abi/5.0>,
include <tunables/global>
/usr/local/bin/bench-report {
include <abstractions/base>
include <abstractions/bash>
include <abstractions/user-tmp>
/etc/hostname r,
/etc/ld.so.cache r,
/proc/filesystems r,
/usr/bin/dash ix,
/usr/bin/gnucp mrix,
/usr/lib/cargo/bin/coreutils/cat mrix,
/usr/lib/cargo/bin/coreutils/date mrix,
/usr/local/bin/bench-report r,
owner /proc/*/maps r,
}
Note that genprof offered include <abstractions/user-tmp> as option 1 for the write to /var/tmp/bench-report.txt, and accepting the default took it. That abstraction grants owner read and write across the temp directories, which is wider than the single file the script needs. For a service you care about, pick the narrower numbered option instead of pressing A on the default.
4. Read AppArmor Denials in journalctl, dmesg and audit.log
With the profile enforcing, the --id path of the script is not covered, so the read of /etc/machine-id fails. The program sees an ordinary “Permission denied”, and only the kernel audit line tells you AppArmor was the cause:
/usr/local/bin/bench-report --id
sudo journalctl -k -g 'apparmor="DENIED"' --no-pager -n 1
Without auditd, the denial lands in the kernel log, so journalctl -k and dmesg both show it:

Every field in that line answers a question you need for the fix:
- apparmor=”DENIED”: the access was blocked; in complain mode the same event reads ALLOWED, and audit mode adds AUDIT lines for permitted access.
- profile: the profile that made the decision, here
/usr/local/bin/bench-report, even thoughcomm="cat"shows the child process that tripped it. - operation and name: the action (
open,exec,capable) and the path or object it touched. - requested_mask and denied_mask: the permission letters the rule must grant,
rhere; anexecdenial showsxand needs anix,PxorCxrule. - fsuid and ouid: the caller’s UID and the file owner’s UID, which tell you whether an
ownerrule would match.
The fix for this denial is one line, /etc/machine-id r,, added with aa-logprof in the next section or by hand followed by apparmor_parser -r.
After auditd is installed, denials leave the kernel log
Installing auditd changes where the lines go. On all three systems, the same denial triggered with auditd running produced zero matches in dmesg and “– No entries –” from journalctl -k, while /var/log/audit/audit.log held the record with type=AVC in place of type=1400:
sudo grep 'apparmor="DENIED"' /var/log/audit/audit.log | tail -1
The Ubuntu 24.04 record carries the enriched user names at the end:
type=AVC msg=audit(1793770355.856:1238): apparmor="DENIED" operation="open" class="file" profile="/usr/local/bin/bench-report" name="/etc/fstab" pid=5673 comm="cat" requested_mask="r" denied_mask="r" fsuid=1001 ouid=0FSUID="user" OUID="root"
The ouid=0FSUID= run-together is not a typo: auditd appends the enriched names after an invisible 0x1d separator, which most terminals do not display. That matters less than where the line went, and the location matters for every log-reading tool. A monitoring rule that greps the journal for DENIED goes silent the day someone installs auditd, so check which one the host runs with systemctl is-active auditd before you trust an empty result.
ausearch needs –input-logs when stdin is not a terminal
When ausearch runs from a script, cron or ssh host 'command', stdin is not a terminal and it reads stdin instead of the audit log. Over a non-interactive SSH session on Ubuntu 26.04, sudo ausearch -m AVC -ts recent printed <no matches>. The same search with --input-logs returned the AppArmor records, and on Ubuntu 24.04 it found both denials of our test read:
sudo ausearch -m AVC -ts recent --input-logs
This works on Ubuntu because its auditd package carries a patch for Launchpad bug 1117804. The changelog in /usr/share/doc/auditd/changelog.Debian.gz lists it on both releases as “Fix AppArmor AVC events not appearing in ausearch”. On Ubuntu 26.04 it arrived in the 1:4.1.2-1ubuntu0.1 update, so run sudo apt upgrade before relying on it. Debian 13 ships auditd without that patch, and section 8 shows what its ausearch does with AppArmor records.
For a human-readable summary, aa-notify from the apparmor-notify package groups events by profile and operation. It is not pulled in by apparmor-utils, which is why the install line in section 1 names it. On Ubuntu 26.04 it kept reading /var/log/kern.log after auditd was installed and reported “AppArmor denials: 0”. aa-notify 5.0.2 looks for /var/run/auditd.pid, while auditd 4.1.2 writes /run/audit/auditd.pid, so pass the audit log explicitly there:
sudo aa-notify -s 1 -v -f /var/log/audit/audit.log
With the file named, it listed only the two user namespace denials from that day:
Using log file /var/log/audit/audit.log
Profile: unconfined
Operation: userns_create
Logfile: /var/log/audit/audit.log
Profile: unconfined
Operation: userns_create
Logfile: /var/log/audit/audit.log
AppArmor denials: 2 (since Tue Nov 3 05:32:37 2026)
For more information, please see: https://wiki.ubuntu.com/DebuggingApparmor
That list is incomplete. The same audit.log held DENIED records for /usr/local/bin/bench-report, and a repeat run with three of them in the log printed “AppArmor denials: 3” while every entry it showed was userns_create; not one named the bench-report profile. On Ubuntu 24.04 and Debian 13, aa-notify picked /var/log/audit/audit.log on its own and did list the bench-report denials by profile. Its total is not a DENIED count, though. It also includes ALLOWED and AUDIT events. It printed 174 on 24.04 and 148 on Debian 13, while grep found 3 and 4 DENIED lines for the profile. On 26.04, count denials for one profile with sudo grep -c 'apparmor="DENIED".*profile="/usr/local/bin/bench-report"' /var/log/audit/audit.log instead, which returned 3 there.
5. Fix Denials with aa-logprof
aa-logprof reads the same events and proposes a rule for each one. Run it after the denial has been logged:
sudo aa-logprof
On Ubuntu 24.04 it found the /etc/machine-id read and offered two choices:

Option 1 is selected by default, and it is the wrong pick for a server. abstractions/dbus-session-strict grants /etc/machine-id r, but it also allows D-Bus session bus sockets under /tmp/dbus-*, @{run}/user/*/bus and several D-Bus method calls. Press 2 to select /etc/machine-id r,, then A to allow it and S to save. Our scripted runs accepted option 1, and the script then printed the machine ID on all three systems, so either choice fixes the denial; option 2 simply grants less.
The bigger lesson came from Ubuntu 26.04. aa-logprof reads every AppArmor event in the log, not only the profile you are working on. Our first scripted run accepted every suggestion. It added capability perfmon and three owner write rules on journal and notify socket paths to the stock systemd-detect-virt profile. It also added owner /etc/passwd r and owner /etc/nsswitch.conf r to the stock who profile, because the log already held a denial of /etc/passwd by that profile. Edits like that land in the distro files under /etc/apparmor.d, where the next package upgrade puts dpkg in conflict with them.
So answer I (Ignore) for any profile that is not yours; the file and capability prompts offer Ignore on all three releases. The exception is an Execute prompt on 24.04, which has no Ignore and where I means Inherit, so press F (Finish) there. Know that F ends the whole session. Questions about your own profile that had not come up yet are skipped too. Do not press D there: Deny, shown in brackets as the default, writes a deny rule into the distro file, which is the edit you are trying to avoid. Put changes to a stock profile in its local override file instead. Ubuntu’s stock profiles include local/<name> for exactly this; the rsyslogd profile on 24.04 carries #include <local/usr.sbin.rsyslogd> on line 66. Add the rule there and reload:
sudo vim /etc/apparmor.d/local/usr.sbin.rsyslogd
After saving the rule, replace the loaded profile from its main file so the override is compiled in:
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.rsyslogd
For an unattended fix, sudo aa-logprof -a accepts every suggestion silently, which only makes sense on a host where the log contains nothing but the program you are profiling. The -a option exists on Ubuntu 26.04 and Debian 13; the AppArmor 4.0.1 aa-logprof --help on Ubuntu 24.04 does not list it.
6. Load, Replace and Test Profiles with apparmor_parser
The aa-* mode commands are wrappers; apparmor_parser is what compiles a profile and loads it into the kernel. After editing any file in /etc/apparmor.d, check it first and then replace the loaded copy:
sudo apparmor_parser -Q /etc/apparmor.d/usr.local.bin.bench-report
sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.bench-report
Both commands print nothing and return 0 when the profile is valid. Errors are explicit, and we provoked the three most common ones on purpose:

-a refuses to overwrite a loaded profile with exit code 239, so use -r for anything already in the kernel. A rule without its trailing comma gives “missing an end of line character?”, and an invalid permission letter (rz in our test) gives “unexpected TOK_ID, expecting TOK_MODE”. Both name the line number, which is where to look first.
Removing with -R took the profile out of /sys/kernel/security/apparmor/profiles (the grep count dropped to 0), and -C loaded it back as (complain) without touching the flags in the file. That makes -C handy for a quick test, but the next systemctl reload apparmor or reboot loads the file’s own mode again, so use aa-complain when the change must persist.
7. AppArmor Profile Syntax Reference
This profile uses the rule types from the quick-reference table in one file. It is not a working profile for a real program. We wrote it to confirm the syntax, and apparmor_parser -Q accepted it with exit code 0 on Ubuntu 26.04, Ubuntu 24.04 and Debian 13:
abi <abi/4.0>,
include <tunables/global>
profile syntax-demo /usr/local/bin/syntax-demo flags=(complain) {
include <abstractions/base>
include <abstractions/nameservice>
capability net_bind_service,
network inet stream,
network inet6 stream,
/usr/local/bin/syntax-demo mr,
/etc/syntax-demo/** r,
/var/log/syntax-demo/*.log w,
/var/lib/syntax-demo/{data,cache}/ rw,
owner @{HOME}/.config/syntax-demo/** rw,
/usr/bin/curl Px,
/usr/bin/sed ix,
deny /etc/shadow r,
include if exists <local/syntax-demo>
}
The header line profile syntax-demo /usr/local/bin/syntax-demo gives the profile a short name and attaches it to the binary path, and apparmor_parser -N printed syntax-demo for it. In paths, * matches within one directory, ** matches across subdirectories, and {a,b} lists alternatives. Every rule ends with a comma, which is the mistake behind most parser errors.
A common search on this page is what mrix means, because genprof writes it for every helper binary. It is three permissions: m lets the program map the file as executable code, r lets it read the file, and ix runs it inside the calling profile. If you would rather the helper run under its own profile, replace ix with Px, and the helper then needs a profile of its own or the exec is denied.
For the theory behind these rules alongside SELinux, chapter 10 of Mastering Linux Security and Hardening (paid link) (third edition, Packt, 2023) by Donald A. Tevault covers mandatory access control with both, and it was in stock when we checked in November 2026. The upstream AppArmor wiki and the apparmor.d(5) man page are the reference for every rule type not listed here.
8. Ubuntu 26.04 vs 24.04 vs Debian 13 Differences
Most AppArmor guides assume Ubuntu’s behaviour everywhere. These differences came from running identical commands on the three VMs, so treat any result on one distro as unproven on another until you run it there:
| Item | Ubuntu 26.04 | Ubuntu 24.04 | Debian 13 |
|---|---|---|---|
| AppArmor userspace | 5.0.2 | 4.0.1 | 4.1.0 |
| /var/log/syslog for genprof | Yes (rsyslog) | Yes (rsyslog) | No |
| genprof inherit key | In(h)erit | (I)nherit | In(h)erit |
| cat binary seen by the profile | /usr/lib/cargo/bin/coreutils/cat | /usr/bin/cat | /usr/bin/cat |
| kernel.apparmor_restrict_unprivileged_userns | 1 | 1 | sysctl absent |
| kernel.apparmor_restrict_unprivileged_unconfined | 1 | 0 | 0 |
| unshare -Ur whoami as a user | Fails | Fails | Prints root |
| ausearch on AppArmor AVC records | Works (auditd 4.1.2, Ubuntu patch) | Works (auditd 3.1.2, Ubuntu patch) | Skips them (auditd 4.0.2, no patch) |
| aa-notify default log with auditd | /var/log/kern.log | audit.log | audit.log |
| aa-notify lists file denials by profile | No (userns only) | Yes | Yes |
| aa-logprof -a (allow all) | Yes | Not available | Yes |
The apparmor_restrict_unprivileged_userns switch is Ubuntu kernel behaviour: Debian 13’s 6.12 kernel does not have that sysctl, so an unprivileged unshare -Ur works there and fails on both Ubuntu releases. The _unconfined sysctl does exist on Debian 13 and reads 0, as on Ubuntu 24.04. Ubuntu 26.04 turned on apparmor_restrict_unprivileged_unconfined. Its audit log showed what that does to aa-exec: an unprivileged aa-exec -p /usr/local/bin/bench-report logged “change_profile unprivileged unconfined converted to stacking”, so the profile was stacked on top of the unconfined label rather than replacing it. The confined cat was still denied the read of /etc/fstab. The profile applied either way.
Debian 13 caused the two failures that waste the most time. This screenshot combines them: the genprof error came before auditd was installed, the audit.log and ausearch lines after:

The Debian 13 image has no rsyslog, so aa-genprof and aa-logprof stop before doing anything. Once auditd was installed, genprof switched to /var/log/audit/audit.log on its own and the whole profile build worked. The ausearch result is worse because it looks like a clean answer: grep found the DENIED line in audit.log, but ausearch 4.0.2 printed <no matches>, and only --debug showed why. It reported “Malformed event skipped, rc=9” for every AppArmor record, 143 of them in the last ten minutes of the log. Upstream ausearch expects a tclass= field that AppArmor records do not carry, and the Ubuntu patch from section 4 is what removes that requirement there. On Debian 13, read audit.log with grep or aa-notify (which listed the bench-report denials by profile there) and do not treat an empty ausearch as proof that nothing was denied.
9. Common AppArmor Errors and Their Fixes
Each heading below is the exact text we got from a lab VM, with the fix we confirmed on the same machine.
ERROR: Can’t find system log “/var/log/syslog”. Please check permissions.
Seen on Debian 13 from both aa-genprof and aa-logprof. Either install auditd so the tools read /var/log/audit/audit.log, or export the kernel journal to a file and pass it with -f. The export route only works while auditd is not running, because auditd takes the denials away from the journal:
sudo journalctl -k --no-pager -o short > /tmp/kern-export.log
sudo aa-logprof -f /tmp/kern-export.log
We tested this on a program with no profile yet. sudo aa-autodep /usr/local/bin/demo3 wrote a skeleton, aa-enforce loaded it, and the first run denied the exec of cat. Our first export, taken in the same command that stopped auditd, held no demo3 line. In a second run we confirmed systemctl is-active auditd printed “inactive”, waited three seconds and triggered the denial again; that export held two lines, and sudo aa-logprof -f /tmp/kern-export.log -a added /usr/bin/cat mrix,. The next run then hit the read of /etc/hostname, so in enforce mode each pass fixes one layer. That is why genprof starts in complain mode: in our genprof runs, one complain-mode pass logged every access the bench-report script made, so run aa-complain on the skeleton before you exercise the program.
apparmor filesystem is not mounted.
aa-status prints this when AppArmor was turned off at boot, and aa-enabled confirms that it was disabled at boot:

Look for apparmor=0 in /proc/cmdline, remove it from GRUB_CMDLINE_LINUX in /etc/default/grub, run sudo update-grub and reboot. On our Ubuntu 26.04 VM that brought back “Yes” from aa-enabled and 202 loaded profiles.
unshare: write failed /proc/self/uid_map: Operation not permitted
On Ubuntu 24.04 and 26.04, an unprivileged process that creates a user namespace is moved into the unprivileged_userns profile, which denies capabilities inside it. Rootless containers, sandboxed browsers and build tools hit this first:

The supported fix is a profile for the program that grants userns,, which is what the 74 to 91 “unconfined” profiles in the earlier count do for desktop apps. Setting kernel.apparmor_restrict_unprivileged_userns=0 removes the restriction for every program on the host, so keep it for a lab box.
apparmor_parser: Unable to add “…”. Profile already exists
The profile is already loaded and -a only adds new ones. Use sudo apparmor_parser -r FILE, which loads or replaces.
missing an end of line character? (entry: /etc/hostname)
A rule on or before the reported line lacks its trailing comma. Add it and rerun apparmor_parser -Q until it returns silently.
10. Disable AppArmor Completely (and Why systemctl stop Is Not Enough)
Stopping the service does not unload anything. On both Ubuntu releases, sudo systemctl stop apparmor left the unit “inactive”, and on all three systems aa-status --profiled still counted every profile afterwards (203 on 26.04 and 141 on 24.04 in the first run, 128 on Debian 13 in a repeat). To unload profiles until the next reload, use aa-teardown:
sudo aa-teardown
sudo aa-status --profiled
On Ubuntu 26.04 that cleared the kernel completely and aa-status answered “No policy loaded into the kernel”. On Ubuntu 24.04 the same command printed a long run of /lib/apparmor/apparmor.systemd: 273: printf: printf: I/O error lines. It left 91 profiles loaded, so check the count afterwards instead of trusting the exit code. sudo systemctl start apparmor reloaded the full set on both. Be equally careful with aa-remove-unknown on 24.04: its dry run (-n) listed all 91 stock “(unconfined)” profiles as unknown, while the 26.04 version listed only two :glycin: profiles and Debian 13 listed none.
To keep AppArmor off across reboots, add apparmor=0 to the kernel command line. Ubuntu cloud images set GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub.d/50-cloudimg-settings.cfg, which overrides the main file, so append to GRUB_CMDLINE_LINUX instead:
sudo sed -i 's/^GRUB_CMDLINE_LINUX="\(.*\)"/GRUB_CMDLINE_LINUX="\1 apparmor=0"/' /etc/default/grub
sudo update-grub
sudo reboot
After the reboot the kernel’s /sys/kernel/security/lsm list no longer contained apparmor, and apparmor.service reported “start condition unmet”. Disabling one profile with aa-disable is almost always the better answer, and the AppArmor configuration guide for Ubuntu 26.04 shows how to write a proper profile for a service that keeps getting blocked. Ubuntu’s own AppArmor documentation describes the same switch.
What to Watch in the Logs After You Enforce a Profile
A new profile rarely fails on day one, because day one only exercises the paths you tested in genprof. The failures come from the code paths you did not run: log rotation, a monthly cron job, a reload signal or an upgrade that moves a binary. In our test the --id branch of an eight-line script was enough to produce a denial the first time it ran in enforce mode.
For the first weeks after enforcing a profile, check these on a schedule:
- DENIED count per profile:
grepforapparmor="DENIED"plus your profile name in/var/log/audit/audit.logon an auditd host, orjournalctl -k -g 'apparmor="DENIED"'without it. aa-notify missed file denials on Ubuntu 26.04, so do not count with it there. A count above zero for your profile is a missing rule or an attack. - exec denials after package upgrades:
denied_mask="x"usually means a binary path changed, as the coreutils move on Ubuntu 26.04 shows. - Profile mode drift:
sudo aa-status --complainingshould not grow; a profile someone flipped to complain mode during an incident and never flipped back blocks nothing. - userns_create and capable denials: on Ubuntu these point at a container or sandbox tool that needs its own
userns,profile, not at your service.
Put these checks into the same review as your firewall rules from the UFW guide and your SSH access, which the SSH commands cheat sheet covers, so that a profile stuck in complain mode gets noticed. On a new server, the things to do after installing Ubuntu 26.04 server checklist puts AppArmor in context with updates and SSH hardening. The full set of our cheat sheets has printable versions for the other tools on the same box.
Very good and informative ! Thank you !
Happy to hear this.. Thank you!