Test and Load AppArmor Profiles Safely with apparmor_parser
You will finish with a small AppArmor profile that you can syntax-check 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 permits. You need a shell and the AppArmor package. A syntax check is unprivileged. Loading, replacing or removing kernel policy normally requires elevated privilege and can change what a process is allowed to do. Read each warning before running a command with sudo.
1. Confirm the installed parser
Start with a read-only version check. This also catches the common distraction of 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 a 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 file rules form an allow-list. The profile below lets a program read one file and execute /usr/bin/true. The trailing comma is part of the rule syntax.
Create a temporary file while learning. It does not alter 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
The shell redirections above only create temporary test data. In a real profile, use full paths and check every rule against the program's actual behaviour. A rule ending in / applies to a directory, while variables, includes and aliases must obey the preamble rules in apparmor.d(5).
Checkpoint: inspect exactly what will be sent 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
Use --skip-kernel-load for a dry run. It performs the parser work but skips the actual kernel load, so it is the right first check for a new or edited profile:
$ apparmor_parser --skip-kernel-load --skip-cache --verbose "$tmp_profile"
Addition succeeded for "demo-parser".
The exact diagnostic wording can vary, but the command should return status 0. Capture it immediately if you are scripting:
$ apparmor_parser --skip-kernel-load --skip-cache "$tmp_profile"
$ parser_status=$?
$ printf 'parser status: %s\n' "$parser_status"
parser status: 0
--skip-kernel-load is not a weak version of loading. The manpage says it performs all actions except the actual kernel load and removes the privilege requirement for kernel policy management. It does not prove that the kernel will accept a profile on this machine, so treat the first real load as a separate checkpoint.
4. Inspect includes and profile names
Before loading a larger policy, see the preprocessed input. This flattens includes and writes the result to standard output:
$ apparmor_parser --preprocess "$tmp_profile"
Use this when an include, variable or relative path makes the source difficult to review. Do not mistake the output for a kernel load: --preprocess is an unprivileged profile command.
List the profile names the parser finds without changing policy:
$ apparmor_parser --names "$tmp_profile"
demo-parser
This is useful when a file contains several profiles or when the profile name is not the same as the file name. A directory argument makes the parser try each non-hidden file except recognised package backup and temporary suffixes. Prefer an explicit file while testing so an unrelated file cannot distract you.
5. Load a new profile only after the dry run
Loading is a security-sensitive state change. It adds the definition to the running kernel and can affect the next process that enters demo-parser. Confirm the source, the profile name and the intended rules first. Then use elevated privilege:
$ sudo apparmor_parser --add --skip-cache --verbose "$tmp_profile"
Addition succeeded for "demo-parser".
The default action is also --add. Naming it explicitly makes a review easier and prevents a reader from confusing this operation with replacement.
Verify that the definition is present by asking the parser to list the same input. This is a parser-side check, not a complete audit of running processes:
$ apparmor_parser --names "$tmp_profile"
demo-parser
If the profile name already exists, add fails rather than silently overwriting it. That boundary is helpful: do not work around it by switching to --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 that 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"
If the new policy causes a problem, restore the saved file and replace the loaded definition:
$ cp -- "${tmp_profile}.known-good" "$tmp_profile"
$ sudo apparmor_parser --replace --skip-cache --verbose "$tmp_profile"
Removal is more disruptive because processes that relied on the definition lose that profile. The parser still requires a complete AppArmor definition, even though it does not use the definition's contents for the removal:
$ sudo apparmor_parser --remove --skip-cache --verbose "$tmp_profile"
Use removal only when you have checked which service or process uses the profile and have a re-load command ready. Re-running --add is not the undo operation for an existing definition; use --replace with the known-good source.
7. Keep cache behaviour visible
The parser normally reads a newer cached profile when one exists, and cache writes are off by default. During a focused test, --skip-cache disables caching altogether. This removes a stale-cache variable from the result, at the cost of compiling the policy each time.
For normal deployments, decide separately whether to write caches with --write-cache. The default cache location is /var/cache/apparmor. Use --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 clears cached profiles unconditionally.
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.
Clean up only the temporary files from this guide when you are finished:
$ rm -f -- "$tmp_profile" "${tmp_profile}.known-good" /tmp/apparmor-parser-input