Automating AppArmor profile management: batch mode switching with aa-enforce and aa-complain, profile merging with aa-mergeprof, syntax validation without loading into the kernel with apparmor_parser, and integrating profile linting and compilation into a CI pipeline along with a multi-distro test matrix.

In episode 17 you broke through to the advanced level: change_hat, named profiles, all the way to stacking between mechanisms. Writing precise profiles is like having the perfect recipe — but in a production team, a good recipe is useless if it only lives in one cook's head. It has to be written down, standardized, and re-tested every time something changes. That's what we cover today: turning profile-writing practice into a workflow that's repeatable, validated, and automated.
In episode 5 you already used aa-genprof and aa-logprof to build profiles interactively. Episode 18 completes that with three layers of automation: batch mode switching with aa-enforce and aa-complain, profile merging with aa-mergeprof, and side-effect-free validation with apparmor_parser -Q. The three are then wired into a CI pipeline that keeps profiles healthy before they reach production.
Throughout this series you've been switching back and forth between complain (only records) and enforce (actually blocks). Manually, that means editing flags and then reloading the profile — error-prone work, especially across many hosts at once. These two tools compress it into a single command.
aa-complain and aa-enforce accept one or more target executables, find their profiles in the profile directory (default /etc/apparmor.d, changeable with the -d option), adjust the mode, then reload into the kernel.
sudo aa-complain /usr/sbin/nginx /usr/sbin/cupsdsudo aa-enforce /usr/sbin/nginx /usr/sbin/cupsdBoth commands are batch: switching several executables at once is faster and less error-prone than editing files one by one. If you only want to change the mode on the profile file without reloading into the kernel — for example so the change can be committed and reviewed first — use the --no-reload option.
To move all custom profiles to complain in one go, a simple loop is enough:
for p in /etc/apparmor.d/usr.*; do
sudo aa-complain "$p"
doneTip
In automation, mode switching is a gate, not a goal. Don't commit the result of aa-enforce without passing through a complain period and a denial review first. The correct flow is always: complain, test, collect denials, fix the profile, then enforce. We'll dissect this phased rollout pattern deeper in episode 20.
After an application is upgraded or its behavior changes, new denials appear. Instead of opening aa-logprof for a single profile, aa-mergeprof merges the observations into one or more profile files at once — ideal for batch post-upgrade updates.
sudo aa-mergeprof /etc/apparmor.d/usr.sbin.nginxThis tool reads the existing profile, compares it with the latest denials from the logs, then offers rule additions to accept or reject. Because all changes are written back to the file, the result can be committed immediately and its diff reviewed. That's where its core value lies: no silent kernel changes that aren't reflected in git.
Testing a profile doesn't always mean loading it into the kernel. The -Q option (short for --skip-kernel-load) makes apparmor_parser run the entire compilation stage except loading into the kernel. This is ideal for CI: no root needed, no kernel state changed, and it returns a non-zero exit code if there's an error.
apparmor_parser -Q /etc/apparmor.d/usr.sbin.nginxTo catch more issues, combine it with --Werror, which treats warnings as errors — for example --Werror=deprecated to reject deprecated syntax:
apparmor_parser -Q --Werror /etc/apparmor.d/usr.sbin.nginxIf you only want to check syntactic correctness, the -d option is also available. The difference is that -Q completes the whole compilation pipeline including the cache stage — so what's tested is exactly what would be loaded into the kernel, not just grammar.
The value of the validation above only becomes real when wired into a pipeline. There are three layers we recommend.
First, lint profiles on every pull request. Every profile change is immediately validated with apparmor_parser -Q --Werror, and its diff is reviewed like ordinary code.
Second, compile the whole profile directory. Loading all files at once catches errors that only appear from interaction between profiles.
Third, a multi-distro test matrix. AppArmor is userspace built and packaged differently per distro, and each distro kernel supports different features. A profile valid on Ubuntu isn't necessarily going to behave the same on Debian or openSUSE.
name: apparmor-policy
on:
pull_request:
paths:
- "profiles/**"
jobs:
validate:
strategy:
fail-fast: false
matrix:
include:
- distro: ubuntu
image: ubuntu:24.04
- distro: debian
image: debian:stable
- distro: opensuse
image: opensuse/leap:15.6
runs-on: ubuntu-latest
container:
image: ${{ matrix.image }}
steps:
- uses: actions/checkout@v4
- name: Install AppArmor
run: |
apt-get update
apt-get install -y apparmor apparmor-utils
- name: Validate profiles without kernel load
run: |
for p in profiles/*; do
apparmor_parser -Q --Werror "$p" || exit 1
doneImportant
The multi-distro matrix isn't a formality. Each release's parser and kernel have different features and permission tables — as you'll see for yourself in episode 19 about the AppArmor 5.x transition. Testing on many distros is the cheapest way to find problems before a user reports them.
For behavior testing — not just syntax — the AppArmor project recommends two approaches: autopkgtest to test profiles against packages in the distro, and spread to run end-to-end scenarios across several systems at once. Both can be invoked from the same CI.
In episode 18 you moved profile work from hands to pipeline: aa-enforce and aa-complain for batch mode switching, aa-mergeprof for merging changes, apparmor_parser -Q for side-effect-free validation, and lint, compilation, and a multi-distro matrix integrated into CI.
Key points to take away:
aa-complain and aa-enforce accept many executables at once — use them for repeatable batch switching.aa-mergeprof writes profiles back to files, so every change is recorded in git.apparmor_parser -Q validates without loading into the kernel — a perfect match for CI.In episode 19 we face a big change: AppArmor 5 & the Policy Transition. You'll understand why 5.0 is a short-lived bridge release, what it means for the 4.x profiles you've already built, and how to migrate safely. See you there!