Learn AppArmor - Policy Tooling & Automation
Episode 18 of 23

Learn AppArmor - Policy Tooling & Automation

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.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

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.

Automatic Mode Switching: aa-enforce and aa-complain

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.

Switch several profiles to complain mode
sudo aa-complain /usr/sbin/nginx /usr/sbin/cupsd
Return to enforce after testing
sudo aa-enforce /usr/sbin/nginx /usr/sbin/cupsd

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

Switch all usr.* profiles to complain
for p in /etc/apparmor.d/usr.*; do
    sudo aa-complain "$p"
done

Tip

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.

aa-mergeprof: Merging Profile Changes

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.

Merge the latest denials into the nginx profile
sudo aa-mergeprof /etc/apparmor.d/usr.sbin.nginx

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

Validation with apparmor_parser -Q

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.

Validate a profile without loading it into the kernel
apparmor_parser -Q /etc/apparmor.d/usr.sbin.nginx

To catch more issues, combine it with --Werror, which treats warnings as errors — for example --Werror=deprecated to reject deprecated syntax:

Treat warnings as errors
apparmor_parser -Q --Werror /etc/apparmor.d/usr.sbin.nginx

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

CI Integration: Lint, Compilation, and a Multi-distro Matrix

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.

.github/workflows/apparmor.yml — policy pipeline
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
          done

Important

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.

Conclusion

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.
  • A healthy pipeline has three layers: per-PR lint, whole-directory compilation, and a multi-distro matrix.
  • Test behavior with autopkgtest or spread, not just syntax validation.

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!

Learn AppArmor - Policy Tooling & Automation | Learn AppArmor