Skip to main content

AppArmor Basics: Profile-Based Access Control for Linux

Created: March 5, 2026 CalmOps 19 min read

Introduction

Linux security traditionally relies on discretionary access control (DAC) — file permissions that the resource owner controls. This model has a fundamental weakness: if a user or process is compromised, the attacker inherits all the owner’s privileges. Mandatory access control (MAC) systems like AppArmor close this gap by enforcing system-wide policies that neither users nor processes can override.

AppArmor (Application Armor) is a MAC system that confines programs to a minimal set of permitted resources through security profiles. Unlike SELinux’s label-based approach, AppArmor uses path-based confinement — policies reference file paths, network types, and capabilities in a syntax that maps directly to how administrators already think about application security.

In 2026, AppArmor is the default LSM on Ubuntu, Debian, openSUSE, and many derivatives. Its integration with Docker, Kubernetes, and systemd makes it a practical choice for production container security without the complexity overhead of SELinux. This guide covers AppArmor from fundamentals through advanced profiles, container integration, monitoring, and performance tuning.

What is AppArmor?

AppArmor is a Linux Security Module (LSM) that enforces MAC policies by confining programs to limited sets of resources. When a confined program attempts to access a resource outside its defined profile, AppArmor blocks the operation and logs the denial. The key design philosophy is least privilege — every application gets only the resources it demonstrably needs to function.

AppArmor profiles are text files in /etc/apparmor.d/ that define security boundaries per executable. Each profile contains rules specifying:

  • File access: read, write, execute, lock, mmap permissions on specific paths
  • Capabilities: Linux capabilities (CAP_NET_BIND_SERVICE, CAP_SYS_ADMIN, etc.)
  • Network access: protocol, address families, port ranges
  • Mount rules: filesystem mount/unmount permissions
  • DBus access: message bus communication restrictions
  • Signal rules: inter-process signal delivery
  • Ptrace rules: debugging and tracing restrictions

AppArmor intercepts system calls at the kernel level through the LSM hook interface. On every access to a protected resource, the kernel consults the loaded profile cache and makes an allow/deny decision. This happens in the hot path of every system call for confined processes, but the overhead is minimal — typically under 1–3% for most workloads.

AppArmor vs Other LSMs

Feature AppArmor SELinux Smack
Policy model Path-based Label-based Label-based
Profile location /etc/apparmor.d/ Policy modules Fixed labels
Learning mode Yes (complain) No (permissive) No
Primary distros Ubuntu, Debian, SUSE RHEL, Fedora Embedded, Android
Container support Native (Docker, K8s) Native (OpenShift) Limited
Learning curve Moderate Steep Moderate

How AppArmor Works

The LSM Framework

The Linux Security Module (LSM) framework provides hook points throughout the kernel where security decisions can be interposed. AppArmor registers callbacks at these hooks — roughly 150 distinct points covering file operations, task operations, network operations, and inter-process communication.

When a confined process calls open("/etc/shadow", O_RDONLY), the kernel:

  1. Performs standard DAC checks (owner/group permissions)
  2. Calls the AppArmor LSM hook for file open
  3. AppArmor looks up the task’s current profile context
  4. Walks the profile’s file access rules against the requested path
  5. Returns allow or deny (which becomes EACCES/EPERM)

This two-layer model means DAC still applies — AppArmor adds an additional gate on top of traditional permissions. A process needs both DAC and MAC approval to access a resource.

Profile Resolution and Cache

AppArmor profiles are compiled from text format into a binary representation by apparmor_parser. The compiled policies are loaded into the kernel, where they’re stored in a directed acyclic graph (DAG) for efficient pattern matching. Wildcards, globs, and path prefixes are compiled into a minimized automaton that enables O(path-length) lookup.

# View the kernel's loaded policy cache
sudo cat /sys/kernel/security/apparmor/profiles

# Force policy recompilation
sudo apparmor_parser -r /etc/apparmor.d/*

# Check cache status
ls -la /etc/apparmor.d/cache/

Profile System

Profiles are stored as individual files in /etc/apparmor.d/. The filename typically mirrors the executable path with slashes replaced by dots:

Executable path     Profile file
─────────────────────────────────────────
/usr/bin/firefox → /etc/apparmor.d/usr.bin.firefox
/usr/sbin/nginx   → /etc/apparmor.d/usr.sbin.nginx
/usr/bin/python3  → /etc/apparmor.d/usr.bin.python3

A profile file contains one or more profile declarations:

# Full path to the confined executable
/usr/bin/myapp {
  # Include directives
  #include <abstractions/base>

  # File access rules (path + permissions)
  /etc/myapp/config r,
  /var/log/myapp/* w,
  owner ~/** r,

  # Capability rules
  capability net_bind_service,
  capability setgid,

  # Network rules
  network inet stream,
  network inet6 stream,

  # Execute other programs (with transition)
  /usr/bin/python3 px,
}

Rule Syntax Reference

File access permissions use a compact single-character notation:

Permission Flag Description
Read r Open file for reading
Write w Open file for writing (truncate/create)
Append a Append-only write access
Execute ix Execute, inherit profile
Execute px Execute, transition to child’s profile
Execute ux Execute unconfined (dangerous)
Execute cx Execute in a sub-profile (hat)
Link l Create hard links
Lock k File locking (flock/fcntl)
Mmap m Memory-map executable pages
All rwix All of the above

Profile Modes

Every profile operates in one of two enforcement modes:

Enforce Mode: Actively blocks violations and logs them. Used in production.

Complain Mode: Logs violations without blocking. Used for profile development and auditing.

# View all profiles and their modes
sudo aa-status

# Switch a specific profile to complain mode
sudo aa-complain /etc/apparmor.d/usr.bin.nginx

# Switch a specific profile to enforce mode
sudo aa-enforce /etc/apparmor.d/usr.bin.nginx

# Disable a profile (unload from kernel)
sudo aa-disable /etc/apparmor.d/usr.bin.test

# Set all profiles to complain (used for initial setup)
sudo aa-complain /etc/apparmor.d/*

Execution Transitions

When a confined program executes another binary, AppArmor must decide how to handle the profile transition:

Inherit (ix): The child runs under the parent’s profile. Suitable for helper scripts and subprocesses that perform similar tasks.

Profile (px): The child transitions to its own profile (if one exists). This is the standard model for well-defined binary boundaries — each program gets its own confinement scope.

Unconfined (ux): The child runs without any AppArmor confinement. This creates a security gap and should be avoided in production.

Child (cx): The child runs in a sub-profile (hat) of the parent. Used for change_hat scenarios.

/usr/bin/parent {
  /bin/ls ix,           # ls inherits parent profile
  /usr/bin/child px,    # child transitions to its own profile
  /bin/dangerous ux,    # unconfined — avoid this
}

Installing and Managing AppArmor

Installation

AppArmor ships with the kernel on most distributions, but user-space tools need to be installed:

# Debian/Ubuntu
sudo apt update
sudo apt install apparmor apparmor-utils apparmor-profiles apparmor-profiles-extra

# openSUSE
sudo zypper install apparmor-parser apparmor-utils apparmor-profiles

# Arch Linux
sudo pacman -S apparmor

Kernel Configuration

Verify AppArmor is enabled in the kernel:

# Check if the LSM is active
cat /sys/kernel/security/apparmor/profiles | head -5

# Check kernel boot parameters (should include apparmor=1 security=apparmor)
cat /proc/cmdline

# Verify AppArmor is the primary LSM
cat /sys/kernel/security/lsm

If AppArmor isn’t enabled, add apparmor=1 security=apparmor to the kernel command line in /etc/default/grub (Ubuntu/Debian):

sudo sed -i 's/GRUB_CMDLINE_LINUX=""/GRUB_CMDLINE_LINUX="apparmor=1 security=apparmor"/' /etc/default/grub
sudo update-grub
sudo reboot

Basic Management Commands

# Check overall status
sudo aa-status

# List all loaded profiles with their modes
sudo aa-status --verbose

# Reload all profiles from disk
sudo find /etc/apparmor.d -type f -name '*' -exec apparmor_parser -r {} +

# Validate a profile without loading it
sudo apparmor_parser -p /etc/apparmor.d/usr.bin.myapp -n

# Dump a profile from kernel memory (to inspect loaded version)
sudo cat /sys/kernel/security/apparmor/profiles | grep myapp

Creating AppArmor Profiles

Development Workflow

Effective profiles follow a systematic iterative process:

  1. Disable the application’s profile (or create a minimal one)
  2. Run aa-genprof to generate an initial profile skeleton
  3. Switch to complain mode and exercise the app fully
  4. Check logs with aa-logprof to identify access patterns
  5. Refine rules based on observed behavior
  6. Switch to enforce mode and validate functionality
  7. Iterate — applications change, profiles must too

Using aa-genprof

The aa-genprof tool generates profiles interactively:

sudo aa-genprof /usr/sbin/nginx

This creates a skeleton profile and prompts you to:

  1. Run the application in another terminal
  2. Exercise the app’s functionality
  3. Return to aa-genprof to review logged events
  4. Decide for each event: Allow, Deny, Glob, Edit, or Abort

After completing the guided workflow, you’ll have a functional profile that covers the application’s basic access patterns.

Complete Profile: Nginx

Here’s a production-grade Nginx profile:

# /etc/apparmor.d/usr.sbin.nginx
#include <tunables/global>

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

  # Capabilities
  capability net_bind_service,
  capability setgid,
  capability setuid,
  capability dac_override,
  capability chown,
  capability sys_chroot,

  # Nginx binary and modules
  /usr/sbin/nginx mr,
  /usr/lib/nginx/** mr,

  # Configuration files
  /etc/nginx/ r,
  /etc/nginx/** r,
  /etc/nginx/mime.types r,
  /etc/nginx/conf.d/ r,

  # Log files
  /var/log/nginx/ w,
  /var/log/nginx/*.log rw,

  # Web content
  /var/www/ r,
  /var/www/** r,
  /srv/www/ r,
  /srv/www/** r,

  # SSL
  /etc/ssl/ r,
  /etc/ssl/** r,
  /etc/letsencrypt/ r,
  /etc/letsencrypt/** r,

  # PID file
  /var/run/nginx.pid rw,
  /run/nginx.pid rw,

  # Temporary files
  /var/lib/nginx/ r,
  /var/lib/nginx/** rwk,
  /tmp/nginx/** rw,

  # Network access
  network inet stream,
  network inet6 stream,
  network netlink raw,

  # Signal handling
  signal set,
  signal peer=/usr/sbin/nginx,
}

Complete Profile: PostgreSQL

# /etc/apparmor.d/usr.lib.postgresql.bin.postgres
#include <tunables/global>

/usr/lib/postgresql/*/bin/postgres {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/ssl_certs>

  # Capabilities
  capability net_bind_service,
  capability sys_resource,
  capability ipc_lock,
  capability dac_override,
  capability setgid,
  capability setuid,
  capability sys_nice,

  # PostgreSQL binary and libraries
  /usr/lib/postgresql/** mr,
  /usr/share/postgresql/** r,

  # Configuration
  /etc/postgresql/ r,
  /etc/postgresql/** r,

  # Data directory
  /var/lib/postgresql/ r,
  /var/lib/postgresql/** rwk,
  /var/lib/postgresql/*/main/ rw,
  /var/lib/postgresql/*/main/** rwk,

  # Logs
  /var/log/postgresql/ w,
  /var/log/postgresql/*.log rw,

  # Run directory
  /var/run/postgresql/ rw,
  /var/run/postgresql/.s.PGSQL.* rw,
  /run/postgresql/ rw,
  /run/postgresql/.s.PGSQL.* rw,

  # Temporary files and memory
  /dev/shm/ r,
  /dev/shm/PostgreSQL/** rw,

  # Network
  network inet stream,
  network inet6 stream,
  network unix stream,

  # Signal handling
  signal (send) set,
  signal peer=/usr/lib/postgresql/*/bin/postgres,

  # ptrace for pg_stat_activity
  ptrace (read) peer=/usr/lib/postgresql/*/bin/postgres,
}

Using aa-logprof for Iterative Refinement

After switching a profile to complain mode and exercising the application, use aa-logprof to automatically scan logs and add missing rules:

# Put the profile in complain mode
sudo aa-complain /etc/apparmor.d/usr.sbin.nginx

# Exercise the application thoroughly
sudo systemctl restart nginx
curl http://localhost/
# ... run through all application features

# Let aa-logprof analyze logs and build rules
sudo aa-logprof

aa-logprof presents each denied access one at a time, allowing you to:

Action Effect
Allow Add the exact path to the profile
Glob Add a wildcard version of the path
Abort Skip (denial will be re-logged)
Ignore Add to the deny list permanently
Edit Manually edit the rule

Profile Variables and Tunables

AppArmor supports variables for reusable configuration patterns:

# In /etc/apparmor.d/tunables/global
@{HOME} = /root /home/*
@{HOMEDIRS} = /home/*
@{PID} = /run/*.pid
@{PROC} = /proc/*/status /proc/version /proc/sysrq-trigger
@{LOG} = /var/log
@{NGINX_CONF} = /etc/nginx

# Using variables in profiles
/usr/sbin/nginx {
  @{NGINX_CONF}/ r,
  @{NGINX_CONF}/** r,
  @{LOG}/nginx/*.log rw,
}

Custom tunables can be added as separate files:

# /etc/apparmor.d/tunables/myapp
@{MYAPP_HOME} = /opt/myapp
@{MYAPP_LOGS} = /var/log/myapp
@{MYAPP_DATA} = /var/lib/myapp

Advanced Profile Techniques

Hat Profiles (change_hat)

Hats are sub-profiles within a main profile that an application can switch into at runtime. The change_hat() system call allows a confined program to change its security context to a predefined hat. This is particularly useful for server applications that handle multiple privilege levels.

/usr/sbin/apache2 {
  #include <abstractions/base>
  
  /var/www/** r,
  
  # Config parsing phase — full access
  /etc/apache2/** r,
  
  # Hat for admin functions
  ^ADMIN {
    /usr/sbin/apache2ctl px,
    /etc/apache2/** rw,
  }
  
  # Hat for CGI execution
  ^CGI {
    /usr/lib/cgi-bin/** rix,
    /var/www/cgi-bin/** rix,
  }
}

Applications switch hats programmatically:

#include <sys/apparmor.h>

if (change_hat("ADMIN", magic_token) < 0) {
    // Handle error
}
// Now running in ^ADMIN hat

// Return to parent profile
if (change_hat(NULL, magic_token) < 0) {
    // Handle error
}

Named profiles allow a similar pattern but are specified per-executable:

# Named profile, can be attached to any executable
profile my_profile /usr/bin/custom_binary {
  /etc/custom/** r,
  /var/lib/custom/** rw,
}

Deny Rules

Explicit deny rules override allow rules. These are useful for blacklisting specific paths within an otherwise allowed glob:

/usr/bin/myapp {
  /home/** r,                # Allow reading home directories
  deny /home/*/secret.pdf r, # Except this specific file
  deny /home/*/.ssh/** r,    # And SSH keys
  
  /var/www/** rw,            # Allow writing to web root
  deny /var/www/config.php w, # But not the config file
}

Conditional Audit Rules

Use the audit keyword to force logging of allowed operations (useful for compliance monitoring):

/usr/bin/sensitive-app {
  /etc/config/db_password r,          # Allowed silently
  audit /etc/config/db_password r,    # Allowed AND logged
  /var/log/app/access.log rw,         # Standard log access
  audit /var/log/app/audit.log rw,    # Audit log access
}

Mount and Pivot Root Rules

For applications that manage filesystems:

/usr/bin/container-manager {
  # Allow mounting specific filesystems
  mount options=(rw) /dev/sda1 -> /mnt/data/,
  mount fstype=tmpfs,
  mount fstype=proc,
  mount fstype=sysfs,
  
  # Pivot root for containers
  pivot_root,
  
  # Allow unmounting
  umount /mnt/data/,
}

AppArmor and Docker

How Docker Integrates AppArmor

Docker automatically loads a default AppArmor profile (docker-default) for all containers. Additional profiles can be specified per container. When Docker starts a container:

  1. The Docker daemon generates a profile from the container’s security configuration
  2. AppArmor confines the container runtime processes
  3. Each container gets an implicit profile that prevents writing to procfs and other dangerous operations

Checking Docker AppArmor Status

# Check which profile a container is running under
docker inspect --format='{{.AppArmorProfile}}' container_name

# View the default Docker AppArmor profile
sudo cat /etc/apparmor.d/docker-default

# Check if AppArmor is loaded in the Docker daemon
docker info | grep -i apparmor

Custom Profile for Containers

Create a custom AppArmor profile for containers:

# /etc/apparmor.d/containers.myapp
#include <tunables/global>

profile containers.myapp flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  
  # Deny dangerous actions by default
  deny capabilities dac_override dac_read_search,
  deny /sys/** rwklx,
  deny /sys/fs/cgroup/** rwklx,
  
  # Network
  network inet stream,
  network inet6 stream,
  
  # Allow the application to run
  / r,
  /bin/** rix,
  /usr/bin/** rix,
  /usr/lib/** mr,
  /etc/** r,
  
  # Application data
  /data/** rw,
  
  # Logging
  /var/log/myapp/** rw,
  
  # Signal handling within container
  signal (send) set,
  signal peer=containers.myapp,
}

Load the profile and use it with Docker:

# Load the profile
sudo apparmor_parser -a /etc/apparmor.d/containers.myapp

# Run a container with this profile
docker run --rm -it \
  --security-opt apparmor=containers.myapp \
  myapp:latest

# Verify
docker inspect --format='{{.AppArmorProfile}}' $(docker ps -lq)

Disabling AppArmor for a Container

# Run without AppArmor confinement (not recommended for production)
docker run --rm -it --security-opt apparmor=unconfined ubuntu bash

AppArmor and Kubernetes

Pod-Level AppArmor

Kubernetes annotate pods to specify AppArmor profiles:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-apparmor
  annotations:
    # Profile loaded on the node
    container.apparmor.security.beta.kubernetes.io/nginx: localhost/usr.sbin.nginx
    # Or use the runtime default
    # container.apparmor.security.beta.kubernetes.io/nginx: runtime/default
    # Or unconfined
    # container.apparmor.security.beta.kubernetes.io/nginx: unconfined
spec:
  containers:
  - name: nginx
    image: nginx:latest

AppArmor Profile Operator

For Kubernetes clusters in 2026, the AppArmor Profile Operator simplifies profile management. It distributes profiles to nodes and validates their presence before scheduling:

apiVersion: apparmor.crossplane.io/v1alpha1
kind: AppArmorProfile
metadata:
  name: nginx-profile
spec:
  profile: |
    #include <tunables/global>
    profile k8s-nginx flags=(attach_disconnected) {
      #include <abstractions/base>
      network inet stream,
      /etc/nginx/** r,
      /var/www/** r,
      /var/log/nginx/** rw,
    }

Verifying AppArmor in Kubernetes

# Check if AppArmor is enabled on nodes
kubectl get nodes -o json | jq '.items[].status.nodeInfo'

# Verify profile is loaded on a specific node
kubectl exec -it pod-name -- cat /proc/self/attr/current

Monitoring and Auditing

Reading Denial Logs

AppArmor logs denials through the kernel audit subsystem. View them in real-time:

# Watch denials as they happen
sudo journalctl -f -t audit | grep -i apparmor

# View recent denials
sudo journalctl -t audit --since "1 hour ago" | grep -i apparmor | tail -50

# Traditional syslog approach
sudo tail -f /var/log/syslog | grep -i apparmor
sudo tail -f /var/log/kern.log | grep -i apparmor

# Using dmesg
sudo dmesg -w | grep -i apparmor

A typical denial log entry:

audit: type=1400 audit(1712345678.123:456): apparmor="DENIED"
  operation="open" profile="/usr/sbin/nginx"
  name="/etc/shadow" pid=1234 comm="nginx"
  requested_mask="r" denied_mask="r" fsuid=0 ouid=0

Key fields to examine:

Field Meaning
operation System call that was denied (open, exec, mknod, etc.)
profile AppArmor profile name
name Path or resource that was accessed
requested_mask Access mode requested
denied_mask Access mode that was denied
pid Process ID
fsuid File system user ID

Decoding Hex Paths

Some paths in audit logs appear hex-encoded. Use aa-decode:

# Decode a specific hex string
echo '2f686f6d652f757365722f2e636f6e666967' | aa-decode

# Decode entire audit log
sudo journalctl -t audit | grep -i apparmor | aa-decode

Setting Up Centralized Logging

Forward AppArmor denials to a SIEM or centralized logging system:

# Using rsyslog to forward AppArmor events
# /etc/rsyslog.d/50-apparmor.conf
if $programname == 'audit' and $msg contains 'apparmor="DENIED"' then {
  action(type="omfwd"
    target="logserver.example.com"
    port="514"
    protocol="tcp"
  )
  stop
}

Alerting on Denials

Set up automated response to AppArmor denials:

#!/bin/bash
# /usr/local/bin/apparmor-alert.sh
# Run this periodically via cron or systemd timer

DENIALS=$(journalctl -t audit --since "5 minutes ago" \
  | grep -c 'apparmor="DENIED"')

if [ "$DENIALS" -gt 10 ]; then
  # Send alert
  curl -X POST -H "Content-Type: application/json" \
    -d "{\"text\": \"High AppArmor denial rate: $DENIALS in 5 minutes\"}" \
    https://hooks.slack.com/services/YOUR/WEBHOOK/URL
fi

Performance Impact

Overhead Measurement

AppArmor’s performance impact is negligible for most workloads. Profile matching uses a compiled DAG that resolves in O(path-length). Benchmark results:

Workload Without AppArmor With AppArmor Overhead
Nginx static file serving 12,000 req/s 11,850 req/s 1.25%
PostgreSQL queries 5,000 TPS 4,930 TPS 1.4%
Python web app (Flask) 2,000 req/s 1,970 req/s 1.5%
File I/O (dd write) 450 MB/s 448 MB/s 0.4%
Process creation (fork+exec) 1,200/s 1,150/s 4.2%

Tuning for Performance

  • Minimize wildcards: Broad /** rw patterns incur more matching cost than specific paths
  • Use caching: Ensure the binary cache at /etc/apparmor.d/cache/ is populated
  • Limit profile nesting: Deep hat hierarchies add lookup overhead
  • Avoid audit on hot paths: Audit rules force synchronous logging
# Precompile all profiles to populate the cache
sudo apparmor_parser -j 4 /etc/apparmor.d/*

# Check cache performance
ls -l /etc/apparmor.d/cache/

When Performance Matters

For extremely latency-sensitive workloads, consider:

  1. Running only essential profiles in enforce mode
  2. Using complain mode for non-critical applications
  3. Leaving frequently-accessed tools unconfined
  4. Load-testing with AppArmor enabled before production deployment

Troubleshooting

Denial Investigation Workflow

When an application breaks after enabling AppArmor:

# Step 1: Check if AppArmor is blocking
sudo aa-status | grep -E "(complain|enforce)"
sudo journalctl -t audit --since "10 minutes ago" | grep -i apparmor

# Step 2: Temporarily set to complain mode
sudo aa-complain /etc/apparmor.d/profile

# Step 3: Restart the application and test
sudo systemctl restart myapp
# Run through test scenarios

# Step 4: Analyze denied accesses
sudo aa-logprof

# Step 5: After fixing, switch back to enforce
sudo aa-enforce /etc/apparmor.d/profile
sudo systemctl restart myapp

Common Issues and Solutions

Application won’t start after enabling profile

# Solution: Switch to complain mode, check logs
sudo aa-complain /etc/apparmor.d/usr.bin.myapp
sudo systemctl restart myapp
sudo journalctl -t audit --since "1 minute ago" | grep -i apparmor

App starts but can’t write log files

# Add write permission to log directory
# In the profile:
/var/log/myapp/ w,
/var/log/myapp/** rw,

Network services can’t bind to privileged ports

# Add capability
capability net_bind_service,
# And the network permission
network inet stream,

Profile parse errors

# Validate syntax
sudo apparmor_parser -p /etc/apparmor.d/profile.name

# Get verbose error details
sudo apparmor_parser -O no-expr-simplify /etc/apparmor.d/profile.name

# Check for missing includes
ls /etc/apparmor.d/abstractions/

Recovery from Lockout

If a restrictive profile locks you out of SSH or console access:

  1. Network boot: Append apparmor=0 to the kernel command line via GRUB
  2. Recovery mode: Boot into single-user mode which may skip AppArmor
  3. Physical console: Some initramfs configurations allow disabling before profile loading
# At GRUB prompt, edit the kernel line to add:
apparmor=0 security=""  # Completely disables AppArmor

AppArmor vs SELinux

Understanding the strengths of each system helps in choosing the right tool:

Aspect AppArmor SELinux
Policy model Path-based (filesystem paths) Label-based (security contexts)
Profile format Simple text rules Policy modules (TE/MLS)
Learning curve Moderate — map to known paths Steep — requires understanding labels, types, roles
Default distros Ubuntu, Debian, openSUSE Fedora, RHEL, CentOS
Container support Native Docker/K8s Native OpenShift
Policy granularity Very good for applications Excellent for fine-grained control
Audit complexity Simple denial format Complex AVC denial messages
Profile generation aa-genprof, aa-logprof audit2allow, sealert
Maintenance Lower overhead per application Higher overhead, but uniform across system
Community profiles Ubuntu ships apparmor-profiles SELinux policy is distribution-managed

When to Choose AppArmor

  • Ubuntu/Debian shop: AppArmor is default and well-integrated
  • Container-heavy deployments: Docker and K3s have first-class AppArmor support
  • Limited security team: Lower complexity means faster policy development
  • Application-specific containment: You need to confine specific daemons, not the entire system
  • Rapid iteration: Quick complain-mode development suits agile environments

When to Choose SELinux

  • RHEL/Fedora shop: SELinux policies are already comprehensive
  • MLS/MCS requirements: SELinux provides formal multi-level security
  • Enterprise compliance: FedRAMP, NIST 800-53 often expect label-based MAC
  • Desktop/user containment: SELinux handles user-level granularity better
  • Already deployed: Mixed MAC systems add operational complexity

Coexistence

AppArmor and SELinux cannot be loaded simultaneously — the LSM framework selects one primary module at boot. Some distributions allow building both as kernel modules, but only one can enforce policy at a time.

# Check which LSM is active
cat /sys/kernel/security/lsm

# Common values:
# "apparmor"     → AppArmor is the MAC provider
# "selinux"      → SELinux is active
# "apparmor,selinux" → Both compiled, but only AppArmor is enforcing

Integration with Systemd

Systemd can work alongside AppArmor to manage profile loading:

# Create a systemd service to load custom profiles at boot
# /etc/systemd/system/apparmor-custom.service
[Unit]
Description=Load custom AppArmor profiles
After=apparmor.service
Requires=apparmor.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/apparmor_parser -a /etc/apparmor.d/custom/
ExecStop=/usr/sbin/apparmor_parser -R /etc/apparmor.d/custom/

[Install]
WantedBy=multi-user.target

Systemd also provides the AppArmorProfile= directive in service files:

[Service]
ExecStart=/usr/bin/myapp
AppArmorProfile=/etc/apparmor.d/usr.bin.myapp
# RestrictAddressFamilies=AF_INET AF_UNIX
# SystemCallFilter=@system-service

Best Practices

Profile Development

  • Always start in complain mode: Never write a profile from scratch in enforce mode
  • Use abstractions: #include <abstractions/base> covers common patterns you’d otherwise need to rediscover
  • Be specific before being broad: Start with exact paths, widen only when necessary
  • Exercise every feature: Walk through all application functionality during complain mode
  • Document each rule: Add comments explaining why each permission exists — your future self will thank you
  • Test upgrades: Application updates may change file paths or access patterns

Production Deployment

  • Audit new denials weekly: Schedule a cron/systemd timer to review profiles
  • Set up alerting: Notify the team when denial rates spike
  • Role-based profile management: Use version control for all /etc/apparmor.d/ files
  • Gradual rollback plan: Keep the previous profile version loaded in case of issues
  • Profile for first boot: Include profiles in your provisioning pipeline (cloud-init, Ansible, Ignition)
  • Test in staging: Profiles behave differently under load — validate in production-like environments

Security Considerations

  • Never use ux in production: Unconfined execution creates a gap that defeats confinement
  • Don’t overuse px: Each profile transition has a cost; prefer ix for helper scripts
  • Combine with other controls: AppArmor complements seccomp, cgroups, and capabilities
  • Review capability grants: capability sys_admin and capability sys_module are extremely powerful
  • Lock down the profile directory: /etc/apparmor.d/ should be read-only to non-root users
  • Monitor for profile bypass: Processes running unconfined that should be confined indicate configuration drift

Monitoring and Maintenance

# Weekly audit script
#!/bin/bash
# /usr/local/bin/apparmor-audit.sh

echo "AppArmor Audit: $(date)"
echo "================================="

# Check for unconfined processes
UNCONFINED=$(ps auxZ | grep -v unconfined | grep -v LABEL | wc -l)
echo "Confined processes: $UNCONFINED"

# Check profiles that changed mode
sudo aa-status | grep -E "(complain|enforce)" | sort

# Recent denials (last 24 hours)
echo ""
echo "Denials in last 24 hours:"
journalctl -t audit --since "24 hours ago" | grep -c apparmor || echo "0"

# Flag profiles still in complain mode
COMPLAIN=$(sudo aa-status 2>/dev/null | grep -c complain)
if [ "$COMPLAIN" -gt 0 ]; then
    echo "$COMPLAIN profiles still in complain mode"
fi

Conclusion

AppArmor provides a powerful yet approachable MAC implementation for Linux systems. Its path-based policy model maps naturally to how administrators understand application resource requirements, reducing the barrier to entry compared to label-based alternatives.

The profile development workflow — generate, complain, log, refine, enforce — enables systematic security hardening without guesswork. Combined with Docker and Kubernetes integration, AppArmor extends confinement to container workloads, making it a practical choice for both traditional server deployments and modern orchestrated environments.

Key takeaways:

  • Start small: Confine one application at a time, beginning with complain mode
  • Use the tools: aa-genprof, aa-logprof, and aa-status eliminate manual profile writing
  • Monitor continuously: Denials are signals; set up alerting to catch issues early
  • Complement not replace: AppArmor works with firewalls, seccomp, and capabilities for defense-in-depth
  • Stay current: Applications change; review and update profiles on a regular cadence

Resources

Comments

Share this article

Scan to read on mobile