Managing AppArmor across many hosts at once: distributing profiles with Ansible and SaltStack, ensuring the same profile version across the whole fleet, handling differences in kernel behavior, rolling out gradually from complain to enforce per host, and monitoring for regressions.

In episode 19 you understood the AppArmor 5.x transition — parser versions, cache, and policy ABI all changing between releases. Now apply that understanding at real scale: not one server, but dozens to thousands of hosts. The question is no longer is the profile correct, but do all hosts use the same profile, equivalent kernels, and a safe rollout process.
Episode 20 covers AppArmor operations at fleet scale: distributing profiles with config management, version consistency, differences in kernel behavior between hosts, gradual complain-to-enforce rollout, and regression monitoring. This is the episode that bridges "a good profile" and "a profile that survives real production".
The biggest enemy of security policy across many hosts isn't a bad profile — it's drift: the condition where each host is slightly different. One host still uses the old profile, another hasn't been updated, a third got a new kernel. Drift happens because there's a different way to change profiles besides the official process: someone edits /etc/apparmor.d manually, or runs aa-enforce on their own.
The principle you must hold: the profile file on a host is not the source of truth — the source of truth is the repository. The host is merely the rendered result of that repository. As long as this principle is held, drift can be eliminated.
The standard tool for applying the principle above is config management. Both Ansible and SaltStack can copy profiles, then reload only the changed ones.
- name: Deploy AppArmor profiles
hosts: web_servers
become: true
tasks:
- name: Copy profile files
ansible.builtin.copy:
src: "profiles/{{ item }}"
dest: "/etc/apparmor.d/{{ item }}"
owner: root
group: root
mode: "0644"
loop:
- usr.sbin.nginx
- usr.bin.myapp
register: copied
- name: Reload changed profiles
ansible.builtin.command:
cmd: "apparmor_parser -r /etc/apparmor.d/{{ item.item }}"
loop: "{{ copied.results }}"
when: item.changed/etc/apparmor.d/usr.sbin.nginx:
file.managed:
- source: salt://apparmor/profiles/usr.sbin.nginx
- user: root
- group: root
- mode: "0644"
reload apparmor profiles:
cmd.run:
- name: apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
- onchanges:
- file: /etc/apparmor.d/usr.sbin.nginxNotice the same pattern in both tools: copy the file, then reload only what changed with apparmor_parser -r (replace). This idempotency matters — running a playbook or state many times must not produce side effects. This is the same pattern as episode 8 on includes and tunables: any small change must be traceable.
Tip
Avoid restarting the apparmor service to load new profiles on a large fleet. systemctl restart apparmor brings the whole policy down and back up — a window in which hosts are unprotected. Better to reload per profile with apparmor_parser -r, which swaps policy atomically without confinement downtime.
Config management is only half the work. The second half is versioning: all hosts must use the same profile version, not whatever happened to spread around. The practices we recommend:
This version consistency is what makes episode 18 work: the CI matrix tests exactly the same version that will be shipped to hosts.
Even a healthy fleet almost always runs several kernel versions at once — and AppArmor treats the kernel as part of the policy. Policy is compiled against the features the target kernel supports, and the cache is invalidated when features change. The available features can be inspected directly:
sudo aa-features-abi /etc/apparmor.d/cache/.featuresls /sys/kernel/security/apparmor/featuresThe implications are real: a profile using new features (for example from the 5.x era, episode 19) fails to load on an old kernel that doesn't support them. That's why the multi-distro test matrix isn't just a formality — apparmor_parser -Q always compiles against the features of the kernel it runs on, so validation in a CI with a different kernel can mislead you.
The control strategy: make the kernel range consistent. If the fleet must run different kernel versions, make sure each version is tested in the matrix; if possible, pin the image or a minimum kernel across the fleet so AppArmor behavior is predictable.
Once profiles are distributed, the question is: how do you activate enforce without burning the whole fleet? The answer is a phased rollout in four phases:
Each phase switch uses the tools from episode 18:
for p in /etc/apparmor.d/usr.*; do
sudo aa-complain "$p"
donefor p in /etc/apparmor.d/usr.*; do
sudo aa-enforce "$p"
doneWarning
Never enforce the whole fleet at once. If a single legitimate denial slips through — say a cache path that only appears at peak load — the failure spreads to every host within minutes, and rollback has to touch every host too. The canary phase isn't a pleasant process to go through; it's a safeguard far cheaper than a major incident.
The phases above are only useful if you have a way to measure whether the rollout is going well. Regression monitoring works by comparing the count and types of denials before vs after the rollout, per host:
journalctl -k --since today | grep -c 'apparmor="DENIED"'sudo aa-status | head -20Recommended practice: record a denial baseline per profile before the rollout, then alert if a new class of denial appears after enforce — especially on the same profile across several hosts at once (a pattern that signals a systemic problem, not a coincidence). A quick per-host summary can be obtained from aa-status. Combine this with the monitoring from episode 15: a sudden spike in denials on one host might mean the application changed behavior — or worse, someone is trying to break the policy.
In episode 20 you brought AppArmor to fleet scale: the repository as source of truth rendered to each host via Ansible or SaltStack, uniform profile versioning so the CI matrix means something, kernel difference control through feature ABIs, a four-phase rollout from canary complain to fleet enforce, and regression monitoring based on denial baselines.
Key points:
apparmor_parser -r, don't restart the apparmor service.In episode 21 we look ahead: Modern Features & Roadmap — the 4.1.6 and 4.1.7 maintenance releases, aa-notify improvements, Python 3.14 support, the container and Kubernetes ecosystem, and LTS kernel support status. See you there!