Introduction

AppArmor is a Linux mandatory access control (MAC) system that confines programs to a limited set of resources — file access, network capabilities, and system calls. Ansible automates AppArmor profile management: deploy custom profiles, switch between enforce/complain modes, audit application behavior, and harden services across your fleet.

Install and Enable AppArmor

---
- name: Configure AppArmor
  hosts: all
  become: true
  tasks:
    - name: Install AppArmor
      ansible.builtin.package:
        name:
          - apparmor
          - apparmor-utils
          - apparmor-profiles
          - apparmor-profiles-extra
        state: present
      when: ansible_os_family == 'Debian'

    - name: Enable AppArmor
      ansible.builtin.service:
        name: apparmor
        state: started
        enabled: true

    - name: Check AppArmor status
      ansible.builtin.command: aa-status
      register: aa_status
      changed_when: false

    - name: Display AppArmor status
      ansible.builtin.debug:
        msg: "{{ aa_status.stdout_lines[:10] }}"

Deploy Custom Profiles

- name: Deploy custom AppArmor profile
  ansible.builtin.copy:
    dest: /etc/apparmor.d/usr.local.bin.myapp
    content: |
      # AppArmor profile for myapp — managed by Ansible
      #include <tunables/global>

      /usr/local/bin/myapp {
        #include <abstractions/base>
        #include <abstractions/nameservice>
        #include <abstractions/openssl>

        # Binary
        /usr/local/bin/myapp mr,

        # Config files (read only)
        /etc/myapp/ r,
        /etc/myapp/** r,

        # Data directory (read/write)
        /var/lib/myapp/ rw,
        /var/lib/myapp/** rw,

        # Logs
        /var/log/myapp/ rw,
        /var/log/myapp/** rw,

        # Temp files
        /tmp/myapp-* rw,

        # Network
        network inet stream,
        network inet dgram,

        # Deny everything else
        deny /home/** rw,
        deny /root/** rw,
        deny /etc/shadow r,
        deny /etc/passwd w,
      }
    mode: '0644'
  notify: reload apparmor

- name: Load profile in enforce mode
  ansible.builtin.command: aa-enforce /etc/apparmor.d/usr.local.bin.myapp
  changed_when: true

Manage Profile Modes

# Set specific profiles to enforce mode
- name: Enforce critical profiles
  ansible.builtin.command: "aa-enforce {{ item }}"
  loop:
    - /usr/sbin/sshd
    - /usr/sbin/nginx
    - /usr/local/bin/myapp
  changed_when: true

# Set profiles to complain mode (log but don't block)
- name: Set profile to complain mode for testing
  ansible.builtin.command: "aa-complain {{ item }}"
  loop:
    - /usr/local/bin/newapp
  changed_when: true

# Disable a profile
- name: Disable profile
  ansible.builtin.command: "aa-disable {{ item }}"
  loop: "{{ apparmor_disabled_profiles | default([]) }}"
  changed_when: true

Nginx AppArmor Profile

- name: Deploy Nginx AppArmor profile
  ansible.builtin.copy:
    dest: /etc/apparmor.d/usr.sbin.nginx
    content: |
      #include <tunables/global>

      /usr/sbin/nginx {
        #include <abstractions/base>
        #include <abstractions/nameservice>
        #include <abstractions/openssl>
        #include <abstractions/ssl_certs>

        capability net_bind_service,
        capability setuid,
        capability setgid,
        capability dac_override,

        /usr/sbin/nginx mr,

        /etc/nginx/ r,
        /etc/nginx/** r,
        /etc/ssl/ r,
        /etc/ssl/** r,

        /var/www/ r,
        /var/www/** r,

        /var/log/nginx/ rw,
        /var/log/nginx/** rw,

        /run/nginx.pid rw,
        /var/lib/nginx/** rw,

        network inet stream,
        network inet6 stream,
      }
    mode: '0644'
  notify: reload apparmor

Docker Container Profiles

- name: Deploy Docker container AppArmor profile
  ansible.builtin.copy:
    dest: /etc/apparmor.d/docker-mycontainer
    content: |
      #include <tunables/global>

      profile docker-mycontainer flags=(attach_disconnected,mediate_deleted) {
        #include <abstractions/base>

        network,
        capability,
        file,

        deny mount,
        deny /sys/firmware/** r,
        deny /proc/kcore r,

        /app/** r,
        /app/data/** rw,
        /tmp/** rw,
      }
    mode: '0644'
  notify: reload apparmor

# Use with: docker run --security-opt apparmor=docker-mycontainer

Audit and Troubleshoot

- name: Check for AppArmor denials
  ansible.builtin.command: "aa-logprof"
  register: logprof
  changed_when: false
  failed_when: false

- name: Search for denials in logs
  ansible.builtin.shell: "dmesg | grep -i apparmor | grep DENIED | tail -20"
  register: denials
  changed_when: false

- name: Display denials
  ansible.builtin.debug:
    var: denials.stdout_lines
  when: denials.stdout | length > 0

- name: Generate profile from logs
  ansible.builtin.command: "aa-logprof -f /var/log/syslog"
  register: profile_suggestions
  changed_when: false
  failed_when: false

Fleet Status Report

- name: Get AppArmor status across fleet
  ansible.builtin.command: aa-status --json
  register: aa_json
  changed_when: false

- name: Report
  ansible.builtin.debug:
    msg: |
      Host: {{ inventory_hostname }}
      Enforce: {{ (aa_json.stdout | from_json).profiles | dict2items | selectattr('value', 'eq', 'enforce') | list | length }}
      Complain: {{ (aa_json.stdout | from_json).profiles | dict2items | selectattr('value', 'eq', 'complain') | list | length }}

Handlers

handlers:
  - name: reload apparmor
    ansible.builtin.service:
      name: apparmor
      state: reloaded

Troubleshooting

Profile Not Loading

- name: Check profile syntax
  ansible.builtin.command: apparmor_parser -Q /etc/apparmor.d/usr.local.bin.myapp
  register: parse_check
  changed_when: false

Application Blocked

Start in complain mode, run the app, then review logs:

- name: Switch to complain mode for debugging
  ansible.builtin.command: "aa-complain /etc/apparmor.d/{{ profile_name }}"
  changed_when: true

Conclusion

AppArmor confines applications to only the resources they need — Ansible deploys and manages profiles at scale. Use complain mode for profiling new applications, enforce mode for production, and aa-logprof to iteratively refine profiles from real access logs. Deploy profiles for Nginx, Docker containers, and custom applications. Security hardening as code, applied consistently across every server.