Test and Load AppArmor Profiles Safely with apparmor_parser
apparmor_parser will check a profile's syntax, show you what it would load, and only touch the kernel when you tell it to. By the end you will have a small profile you can compile without touching the running kernel, inspect after preprocessing, and load only when you are ready. The examples use apparmor_parser 4.0.1, from package version 4.0.1really4.0.1-0ubuntu0.24.04.7 on the system used for this guide.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about twenty minutes, including time to check what the profile actually permits. You need a shell and the AppArmor package. A syntax check is unprivileged; loading, replacing or removing kernel policy normally needs elevated privilege and can change what a process is allowed to do. Read each warning before running anything with sudo.
1. Confirm the installed parser
Start with a read-only version check. It also catches a common time-waster: following examples written for a different parser release.
$ apparmor_parser --version
AppArmor parser version 4.0.1
Check the option names on the same host if your distribution has patched the utility:
$ apparmor_parser --help | sed -n '1,45p'
Checkpoint: you should see --skip-kernel-load, --skip-cache, --preprocess, --names and the profile commands --add, --replace and --remove.
2. Write a deliberately small profile
AppArmor policy is declarative: a profile has a name followed by braces, and its rules form an allow-list. The one below lets a program read one file and execute /usr/bin/true. The trailing comma is part of the syntax, not a typo.
Create a temporary file while you learn; it never touches the packaged profiles under /etc/apparmor.d:
$ tmp_profile=$(mktemp)
$ cat >"$tmp_profile" <<'EOF'
profile demo-parser {
/tmp/apparmor-parser-input r,
/usr/bin/true ix,
}
EOF
$ printf 'sample\n' >/tmp/apparmor-parser-input
Those shell redirections only create test data. In a real profile, use full paths and check every rule against what the program actually does. A rule ending in / applies to a directory, and variables, includes and aliases must follow the preamble rules in apparmor.d(5).
Checkpoint: inspect exactly what you are about to send to the parser:
$ sed -n '1,20p' "$tmp_profile"
profile demo-parser {
/tmp/apparmor-parser-input r,
/usr/bin/true ix,
}
3. Compile without loading policy
--skip-kernel-load gives you a dry run: the parser does its full job, but skips the actual kernel load. It is the right first check for anything new or edited:
$ apparmor_parser --skip-kernel-load --skip-cache --verbose "$tmp_profile"
Addition succeeded for "demo-parser".
The exact wording can vary, but you should get status 0. Capture that directly if you are scripting it:
$ apparmor_parser --skip-kernel-load --skip-cache "$tmp_profile"
$ parser_status=$?
$ printf 'parser status: %s\n' "$parser_status"
parser status: 0
Tip
--skip-kernel-load is not a weak version of loading: the manpage says it performs every action except the kernel load itself, and it drops the privilege requirement for kernel policy management along with it. It does not prove the kernel will accept the profile on this machine, so treat the first real load as its own checkpoint.
4. Inspect includes and profile names
Before loading a larger policy, look at the preprocessed input. This flattens includes and writes the result to standard output:
$ apparmor_parser --preprocess "$tmp_profile"
Reach for this when an include, variable or relative path makes the source hard to read at a glance. It will not touch the kernel: --preprocess is an unprivileged profile command.
List the profile names the parser finds, again without changing policy:
$ apparmor_parser --names "$tmp_profile"
demo-parser
Handy when a file holds several profiles, or when the profile name does not match the filename. A directory argument makes the parser try every non-hidden file except recognised package backup and temporary suffixes, so prefer an explicit file while testing, so a stray file cannot distract you.
5. Load a new profile only after the dry run
Loading is a security-sensitive change: it adds the definition to the running kernel and can affect the very next process that enters demo-parser. Confirm the source, the profile name and the rules first, then reach for elevated privilege:
$ sudo apparmor_parser --add --skip-cache --verbose "$tmp_profile"
Addition succeeded for "demo-parser".
--add is also the default action; naming it explicitly just makes review easier and stops a reader confusing it with replacement.
Confirm the definition landed by asking the parser to list the same input again. This checks the parser's view, not every running process:
$ apparmor_parser --names "$tmp_profile"
demo-parser
Warning
If the profile name already exists, --add fails rather than silently overwriting it. That boundary is doing you a favour: do not work around it by reaching for --replace until you have compared the old and new policy.
6. Replace or remove with a recovery plan
After editing a loaded profile, use --replace. Keep a copy of the last known-good source so recovery is a deliberate replacement, not a guess:
$ cp -- "$tmp_profile" "${tmp_profile}.known-good"
$ apparmor_parser --skip-kernel-load --skip-cache "$tmp_profile"
$ sudo apparmor_parser --replace --skip-cache --verbose "$tmp_profile"
Recovery
If the new policy causes a problem, restore the saved file and replace the loaded definition again:
$ cp -- "${tmp_profile}.known-good" "$tmp_profile"
$ sudo apparmor_parser --replace --skip-cache --verbose "$tmp_profile"
Removal is the more disruptive option, because any process relying on the definition loses that profile. Oddly, the parser still needs a complete AppArmor definition for this, even though it never uses the definition's contents to do the removing:
$ sudo apparmor_parser --remove --skip-cache --verbose "$tmp_profile"
Only remove a profile once you know which service or process uses it, and have a reload command ready. Running --add again is not the undo for an existing definition; use --replace with the known-good source instead.
7. Keep cache behaviour visible
The parser normally reads a newer cached profile when one exists, though writing new cache entries is off by default. During a focused test, --skip-cache switches caching off entirely: you lose the speed, but also a stale-cache variable you do not need while testing.
For real deployments, decide separately whether to write caches with --write-cache. The default cache location is /var/cache/apparmor. Reach for --show-cache when you need to understand a hit or miss, and --skip-read-cache when a replacement must not read an existing cache. Do not purge the cache casually: --purge-cache is privileged, and it clears cached profiles unconditionally.
8. Clean up the test files
Once you are done, remove only the temporary files this guide created:
$ rm -f -- "$tmp_profile" "${tmp_profile}.known-good" /tmp/apparmor-parser-input
That leaves the loaded demo-parser policy in place until you remove it with step 6's recovery plan, so do that too if this was only ever a test.
Done means
- You checked the installed parser version and options.
- You reviewed a complete
apparmor.d(5)profile with absolute paths. - The profile passed a dry run with
--skip-kernel-load --skip-cache. - You used
--namesor--preprocesswhen the source needed inspection. - You treated add, replace and remove as distinct security changes.
- You kept a known-good source before replacing a loaded profile.