Cheat Sheets

AppArmor Cheat Sheet: Commands for Linux System Administrators

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

Original content from computingforgeeks.com - post 145738

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

CommandWhat it does
aa-enabledPrints Yes, or why AppArmor is off
sudo aa-statusLoaded profiles and confined processes by mode
sudo aa-status --enabledSilent check, exit code 0 when enabled
sudo aa-status --profiledNumber of loaded profiles
sudo aa-status --enforcedNumber of profiles in enforce mode
sudo aa-status --complainingNumber of profiles in complain mode
sudo aa-status --pretty-jsonStatus as formatted JSON
sudo aa-status --filter.profiles='.*tcpdump.*'Show only profiles matching a regex
ps -eo pid,label,commProfile label of every process
sudo aa-unconfinedNetwork-facing processes with no profile
sudo cat /sys/kernel/security/apparmor/profilesRaw kernel list of loaded profiles

Switch profile modes

CommandWhat it does
sudo aa-enforce /usr/bin/tcpdumpEnforce: block and log violations
sudo aa-complain /usr/bin/tcpdumpComplain: allow and log violations
sudo aa-audit /usr/bin/tcpdumpAdd flags=(audit), log allowed access too
sudo aa-audit -r /usr/bin/tcpdumpRemove the audit flag again
sudo aa-disable /usr/bin/tcpdumpUnload profile, link it in disable/
sudo aa-enforce /etc/apparmor.d/usr.bin.tcpdumpSame, by profile file instead of binary
sudo aa-exec -p tcpdump -- cat /etc/hostnameRun a command under a named profile (denied here)

Load and test profiles with apparmor_parser

CommandWhat it does
sudo apparmor_parser -r FILELoad or replace a profile (use after edits)
sudo apparmor_parser -a FILEAdd a new profile, fails if already loaded
sudo apparmor_parser -R FILERemove the profile from the kernel
sudo apparmor_parser -C FILELoad the profile in complain mode
sudo apparmor_parser -Q FILESyntax check only, no kernel load
sudo apparmor_parser -N FILEPrint the profile names in a file
apparmor_parser --versionUserspace version

Build and update profiles

CommandWhat it does
sudo aa-genprof /usr/local/bin/appInteractive profile build from a live run
sudo aa-autodep /usr/local/bin/appSkeleton profile in complain mode
sudo aa-logprofTurn logged events into profile rules
sudo aa-logprof -f /tmp/kern.logRead events from a log file you choose
sudo aa-remove-unknown -nDry run of unloading profiles with no file

Read AppArmor denial logs

CommandWhat 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.logDenials once auditd is running
sudo ausearch -m AVC -ts recent --input-logsLast 10 minutes of AVC records (Ubuntu)
sudo aa-notify -s 1 -vDenials from the last day, readable
sudo aa-notify -s 1 -v -f /var/log/audit/audit.logSame, forcing the audit log

Service and boot control

CommandWhat it does
sudo systemctl reload apparmorReload every profile in /etc/apparmor.d
sudo systemctl stop apparmorStops the unit, profiles stay loaded
sudo aa-teardownUnload profiles, then check the count
apparmor=0 on the kernel command lineDisable AppArmor at boot
sudo sysctl kernel.apparmor_restrict_unprivileged_usernsUbuntu user namespace restriction

Profile syntax quick reference

RuleMeaning
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

LetterMeaning
rRead
wWrite (conflicts with a)
aAppend
mmmap with PROT_EXEC (load as code)
kLock
lLink
ixExecute, inherit the current profile
px / PxExecute under the program’s own profile
cx / CxExecute under a child profile
ux / UxExecute 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:

AppArmor cheat sheet: aa-enabled and aa-status profile counts on Ubuntu 26.04

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 countUbuntu 26.04Ubuntu 24.04Debian 13
Profiles loaded202140127
Enforce mode126457
Complain mode2444
Unconfined mode749176
Processes in enforce mode310

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:

aa-genprof session on Ubuntu 26.04 showing the In(h)erit prompt and Rust coreutils paths

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 cat and date from /usr/lib/cargo/bin/coreutils/ and cp from /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:

AppArmor DENIED audit line for /etc/machine-id read with journalctl -k on Ubuntu 26.04

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 though comm="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, r here; an exec denial shows x and needs an ix, Px or Cx rule.
  • fsuid and ouid: the caller’s UID and the file owner’s UID, which tell you whether an owner rule 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:

aa-logprof offering an abstraction or a single rule for a denied /etc/machine-id read on Ubuntu 24.04

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:

apparmor_parser Profile already exists and syntax error messages

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

ItemUbuntu 26.04Ubuntu 24.04Debian 13
AppArmor userspace5.0.24.0.14.1.0
/var/log/syslog for genprofYes (rsyslog)Yes (rsyslog)No
genprof inherit keyIn(h)erit(I)nheritIn(h)erit
cat binary seen by the profile/usr/lib/cargo/bin/coreutils/cat/usr/bin/cat/usr/bin/cat
kernel.apparmor_restrict_unprivileged_userns11sysctl absent
kernel.apparmor_restrict_unprivileged_unconfined100
unshare -Ur whoami as a userFailsFailsPrints root
ausearch on AppArmor AVC recordsWorks (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.logaudit.logaudit.log
aa-notify lists file denials by profileNo (userns only)YesYes
aa-logprof -a (allow all)YesNot availableYes

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:

Debian 13 aa-genprof cannot find /var/log/syslog and ausearch skips AppArmor AVC records as malformed

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:

aa-enabled reports disabled at boot and aa-status says apparmor filesystem is not mounted

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:

unshare uid_map Operation not permitted and the unprivileged_userns AppArmor denial on Ubuntu 24.04

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: grep for apparmor="DENIED" plus your profile name in /var/log/audit/audit.log on an auditd host, or journalctl -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 --complaining should 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.

Keep reading

UFW Firewall Commands with Examples on Ubuntu 26.04 / 24.04 Security UFW Firewall Commands with Examples on Ubuntu 26.04 / 24.04 PHP2Shell (CVE-2026-109618, CVE-2026-109619): Check and Patch PHP Php PHP2Shell (CVE-2026-109618, CVE-2026-109619): Check and Patch PHP Claude Code Cheat Sheet: Commands, Shortcuts and Flags (PDF) AI Claude Code Cheat Sheet: Commands, Shortcuts and Flags (PDF) APT Cheat Sheet: apt and apt-get Commands for Ubuntu/Debian Cheat Sheets APT Cheat Sheet: apt and apt-get Commands for Ubuntu/Debian macOS Keyboard Shortcuts Cheat Sheet (Golden Gate and Tahoe) Cheat Sheets macOS Keyboard Shortcuts Cheat Sheet (Golden Gate and Tahoe) Borg vs Restic vs Kopia: Backup Tools Benchmarked Debian Borg vs Restic vs Kopia: Backup Tools Benchmarked

2 thoughts on “AppArmor Cheat Sheet: Commands for Linux System Administrators”

Leave a Comment

Press ESC to close