Clean Up Stale AppArmor Profiles with aa-remove-unknown

aa-remove-unknown clears AppArmor profiles that stay loaded in the kernel after their file vanishes from /etc/apparmor.d/. Delete a profile by hand, remove a package, or roll back a deployment, and you can end up with exactly that: a ghost policy with nothing on disk behind it. Budget about ten minutes for a careful review, not a quick run. You need the AppArmor utilities package, a shell, and root for the inventory and removal steps.

This guide covers the installed AppArmor package version 4.0.1really4.0.1-0ubuntu0.24.04.7; the local manual identifies the utility itself as AppArmor 4.0.1. The command changes loaded kernel policy. It does not touch anything under /etc/apparmor.d/.

1. See what the tool actually compares

$ command -v aa-remove-unknown
/usr/sbin/aa-remove-unknown
$ aa-remove-unknown --help
usage: /usr/sbin/aa-remove-unknown [options]

Remove profiles unknown to the system

Options:
 -h, --help	Show this help message and exit
 -n		Dry run; don't remove profiles

Checkpoint: stop here if you expected this to delete obsolete files, disable AppArmor altogether, or edit policy text. It does none of those things.

2. Run the dry run first

The -n option only prints what would be removed, so start there every time:

$ sudo aa-remove-unknown -n
Would remove 'example//null-/usr/bin/whoami'
Would remove 'example'

That output is the manual's sample, not a forecast of yours: what you see depends entirely on the profiles loaded on your host. Empty output is a good result too, it just means nothing loaded is missing from disk.

An unprivileged run cannot read /sys/kernel/security/apparmor/profiles and exits with an error. That is a permissions failure, not proof of a clean system. Use sudo, then check the exit status rather than trusting the absence of text on screen:

$ sudo aa-remove-unknown -n
$ status=$?
$ printf 'dry-run status: %s\n' "$status"
dry-run status: 0

A successful dry run finishes without an AppArmor filesystem error. If it complains that it cannot read the loaded-profile list, stop and fix that before you go anywhere near removal.

3. Investigate every name it flags

Before touching kernel state, chase down each candidate. Start by listing what actually exists on disk:

$ sudo find /etc/apparmor.d -maxdepth 1 -type f -printf '%f\n' | sort
$ sudo aa-remove-unknown -n
$ dpkg-query -S /etc/apparmor.d/PROFILE_FILE

Swap PROFILE_FILE for a real filename from the directory. If a package owns it, this becomes a packaging or service-management decision, not a job for this tool: the comparison is mechanical and has no idea whether a profile is still needed.

Safety warning: removal can weaken confinement for a process that is still running or due to start again. Do not wave a name through just because its file is hard to find. Resolve ownership and lifecycle first.

4. Remove only what you've cleared

Once every flagged name is understood and the maintenance window allows a policy change, drop -n:

$ sudo aa-remove-unknown
Removing 'example//null-/usr/bin/whoami'
Removing 'example'

It reports each profile it removes. There is no flag to narrow this to one profile: it always acts on the full comparison. That is exactly why steps 2 and 3 matter so much.

Checkpoint: save this output with your change record. If it lists a profile you never approved, stop making further changes and preserve the evidence.

5. Undo a removal that turns out to be wrong

There is no undo flag. The command never touches /etc/apparmor.d/, so recovery means restoring or locating the intended profile file, not recreating one from memory:

$ sudo apparmor_parser --replace /etc/apparmor.d/PROFILE_FILE

Common traps

Permission denied: the utility could not read the AppArmor profiles interface. Rerun with the privilege your host requires and confirm AppArmor's securityfs is mounted. Do not read this error as a clean inventory.

Unexpected names: stop before removing anything. Match the name against a profile file and its owning service or package; namespace-qualified names rarely look like the filename you expect.

Parser or profile errors: fix the profile source first. A broken policy inventory is no basis for unloading anything. Rerun the dry run once the source is fixed.

No output: normal, and it means nothing loaded is unknown, but only when the command also exits successfully. Capture the exit status immediately if you're scripting this.

Done means