AppArmor Explained
AppArmor confines applications to exactly the set of files and capabilities they need to function. A confined nginx process cannot read your SSH private keys, cannot write to /etc, and cannot make arbitrary network connections — even if a vulnerability in nginx is exploited. AppArmor is the default MAC system on Ubuntu and Debian and requires far less upfront configuration than SELinux.
Checking AppArmor status
# Detailed status: profiles loaded, their mode, confined processes
sudo aa-status
# Quick summary
sudo apparmor_status
# Check if AppArmor is enabled in the kernel
cat /sys/module/apparmor/parameters/enabled
# View AppArmor events in systemd journal
sudo journalctl -k | grep apparmor
sudo journalctl -k | grep "DENIED"
# View the audit log (if auditd is installed)
sudo tail -f /var/log/audit/audit.log | grep apparmor
Profile modes
Each profile can be in one of three states:
- enforce: violations are blocked and logged
- complain: violations are logged but not blocked (learning mode)
- unconfined: no profile loaded for this program
# Put a profile in enforce mode
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
sudo aa-enforce nginx # short form, resolves the path
# Put a profile in complain mode (for building/testing)
sudo aa-complain /etc/apparmor.d/usr.sbin.nginx
sudo aa-complain nginx
# Disable a profile entirely (unconfine the program)
sudo aa-disable /etc/apparmor.d/usr.sbin.nginx
# Load/reload a profile after editing it
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
# Remove a profile from the kernel
sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.nginx
Profile file structure
AppArmor profiles are text files in /etc/apparmor.d/. Here is an annotated example:
# /etc/apparmor.d/usr.sbin.nginx
#include <tunables/global>
/usr/sbin/nginx {
# Include common abstractions (sets of related permissions)
#include <abstractions/base>
#include <abstractions/nameservice>
# Capabilities the program needs
capability net_bind_service, # bind to ports < 1024
capability setuid, # change user ID
capability setgid, # change group ID
capability dac_override, # bypass file permission checks
# Network access
network inet stream, # TCP over IPv4
network inet6 stream, # TCP over IPv6
# File access: path mode
/etc/nginx/ r, # read the config directory
/etc/nginx/** r, # read all files in config dir
/var/www/html/ r, # read web root
/var/www/html/** r, # read all web files
/var/log/nginx/ rw, # write to log directory
/var/log/nginx/** rw, # write to log files
/run/nginx.pid rw, # write PID file
/tmp/ rw, # temp files
/tmp/** rw,
# Deny access to sensitive paths (explicit deny overrides allow)
deny /etc/shadow r,
deny /root/ r,
deny @{HOME}/.ssh/ r,
}
File permission modes
| Mode | Meaning |
|---|---|
r | read |
w | write |
a | append |
x | execute |
m | mmap |
k | file locking |
l | link |
ux | execute unconfined |
Cx | execute in child profile |
ix | execute and inherit current profile |
Path patterns
/etc/nginx/nginx.conf r, # exact file
/etc/nginx/ r, # the directory itself
/etc/nginx/** r, # everything inside, recursively
/etc/nginx/* r, # immediate children only (not recursive)
@{HOME}/ r, # tunable: expands to /home/*/ and /root/
@{PROC}/@{pid}/status r, # /proc/<pid>/status
Building a profile with aa-genprof
aa-genprof is the recommended way to create a profile for a new application:
# Step 1: Start the profile generator
sudo aa-genprof /usr/sbin/myapp
# Step 2: In another terminal, run the application and use all its features
# Exercise every code path you want to allow
# Step 3: Back in the aa-genprof terminal, press S to scan the log
# It will show you each access and ask Allow/Deny
# Step 4: Press F when done. The profile is saved to /etc/apparmor.d/
# Step 5: Run the app more in complain mode to catch anything missed
sudo aa-complain /usr/sbin/myapp
# ... use the app more ...
# Step 6: Refine with aa-logprof
sudo aa-logprof
# Step 7: Switch to enforce mode
sudo aa-enforce /usr/sbin/myapp
Refining profiles with aa-logprof
aa-logprof reads AppArmor events from the log and prompts you to update the profile:
# Process logged events and update profiles
sudo aa-logprof
# aa-logprof will show each unhandled access and ask:
# (A)llow / (D)eny / (I)gnore / (G)lob / (Q)uit
# Globbing replaces a specific path with a pattern
# Choose (G) to use /var/log/myapp/* instead of individual files
# After responding to all prompts, save the updated profiles
Abstractions
Abstractions are reusable snippets that grant common permission sets. Include them instead of duplicating the rules:
#include <abstractions/base> # basic libc/system calls
#include <abstractions/nameservice> # DNS resolution
#include <abstractions/openssl> # TLS libraries
#include <abstractions/php> # PHP interpreter
#include <abstractions/python> # Python interpreter
#include <abstractions/ssl_keys> # read TLS key material (careful)
#include <abstractions/user-tmp> # write to /tmp and /var/tmp
# List available abstractions
ls /etc/apparmor.d/abstractions/
# View an abstraction
cat /etc/apparmor.d/abstractions/base
Local overrides
To add rules to an existing profile without modifying the package-provided profile file (which could be overwritten on updates), use the /etc/apparmor.d/local/ directory:
# Add rules to nginx profile without touching the main profile
sudo nano /etc/apparmor.d/local/usr.sbin.nginx
# Add your extra rules here (no outer profile block needed):
# /srv/mysite/public/** r,
# /run/php/php8.2-fpm.sock rw,
# The main profile already has: #include <local/usr.sbin.nginx>
# Reload to apply
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
Reading AppArmor denials
# View denials in the kernel log
sudo dmesg | grep -i apparmor | tail -20
# View via journalctl
sudo journalctl -k -g apparmor | tail -30
# A typical denial:
# apparmor="DENIED" operation="open" profile="/usr/sbin/nginx"
# name="/etc/ssl/private/mysite.key" pid=1234 comm="nginx"
# requested_mask="r" denied_mask="r" fsuid=33 ouid=0
# Reading this:
# - nginx (profile /usr/sbin/nginx) tried to open /etc/ssl/private/mysite.key
# - The operation (read) was denied
# - Fix: add /etc/ssl/private/mysite.key r, to the profile
Practical example: confining a custom script
# You have a backup script at /usr/local/bin/backup.sh
# Create a profile for it
sudo aa-genprof /usr/local/bin/backup.sh
# Then run the script in another terminal to generate log events
# Press S to scan, answer the prompts, press F when done
# Resulting profile might look like:
cat /etc/apparmor.d/usr.local.bin.backup.sh
#include <tunables/global>
/usr/local/bin/backup.sh {
#include <abstractions/base>
#include <abstractions/bash>
/usr/local/bin/backup.sh r,
/bin/bash rix,
/usr/bin/rsync rix,
/usr/bin/gzip rix,
/home/@{user}/ r,
/home/@{user}/** r,
/mnt/backup/ rw,
/mnt/backup/** rw,
/var/log/backup.log w,
}
Frequently Asked Questions
What is AppArmor and how does it differ from SELinux?
AppArmor is a mandatory access control (MAC) system for Linux that uses per-application profiles to restrict what files, capabilities, and network resources a program can access. Unlike SELinux, which labels every file and process with a security context and uses type enforcement rules, AppArmor attaches policies to program paths (e.g., /usr/sbin/nginx) and the profiles define what that specific program can do. AppArmor is generally considered easier to configure than SELinux because profiles are written in a readable path-based syntax and tools like aa-genprof and aa-logprof can generate profiles semi-automatically. AppArmor is the default MAC system on Ubuntu, Debian, and openSUSE.
What is the difference between AppArmor enforce and complain mode?
In enforce mode, AppArmor actively blocks any access that the profile does not explicitly allow, and logs the denial. This is the production setting. In complain mode (also called learning mode), AppArmor logs all accesses that would be denied but does not actually block them — the program runs as if no profile were loaded. Complain mode is used to build and refine profiles: you run the application in complain mode, use it normally, and then run aa-logprof to incorporate the logged accesses into the profile. Once the profile covers all legitimate accesses, you switch to enforce mode.
How do I check the status of AppArmor profiles?
Run sudo aa-status to see all loaded profiles and which mode they are in (enforce vs complain), plus a list of which processes are currently confined. For a quick count, sudo apparmor_status gives a summary. You can also check individual profiles: sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx puts nginx in enforce mode, sudo aa-complain /etc/apparmor.d/usr.sbin.nginx puts it in complain mode. Profile files live in /etc/apparmor.d/ and the parsed/cached versions live in /etc/apparmor.d/cache/ or /var/cache/apparmor/.
Where are AppArmor profiles stored?
AppArmor profiles are stored in /etc/apparmor.d/ as plain text files. Each profile file typically corresponds to one confined program and is named after the program path with slashes replaced by dots (e.g., /etc/apparmor.d/usr.sbin.nginx for /usr/sbin/nginx). The directory also contains abstractions/ (reusable snippets included by multiple profiles), tunables/ (variables like @{HOME} defined once and used throughout), and local/ (a place for site-specific additions to existing profiles without modifying the main profile file). Profiles installed by packages are placed here automatically during installation.
How do I write a new AppArmor profile?
The easiest method is to use aa-genprof: run sudo aa-genprof /path/to/program, then in another terminal run the program and exercise all its features. aa-genprof watches the AppArmor log and prompts you to allow or deny each access. When done, it saves a draft profile. Then use the application normally while AppArmor is in complain mode and run sudo aa-logprof to incorporate any additional accesses that were logged. Finally, switch to enforce mode with sudo aa-enforce /etc/apparmor.d/your.profile. Manually writing a profile from scratch is also possible by following the profile syntax and loading it with sudo apparmor_parser -r /etc/apparmor.d/your.profile.
How do I disable AppArmor for a specific program without disabling AppArmor globally?
Create a symlink from the profile to /etc/apparmor.d/disable/ and reload the profile: sudo ln -s /etc/apparmor.d/usr.sbin.nginx /etc/apparmor.d/disable/ followed by sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.nginx. This removes the profile from the running kernel without affecting other profiles. To re-enable it, remove the symlink and reload: sudo rm /etc/apparmor.d/disable/usr.sbin.nginx and sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx. Alternatively, sudo aa-disable /path/to/program does the same thing in one command.