Skip to content

AppArmor: Profiles, Modes & Management

Chapter 17: AppArmor: Profiles, Modes & Management

Section titled “Chapter 17: AppArmor: Profiles, Modes & Management”
  • Understand AppArmor profiles and how they differ from SELinux
  • Know how to read, write, and manage AppArmor profiles
  • Understand complain vs enforce mode
  • Troubleshoot AppArmor denials

AppArmor is a security system (an alternative to SELinux) that locks down applications. You give AppArmor a profile for a program (like a web server) saying ‘this program is only allowed to read these three folders’. If the program gets hacked and tries to read passwords, AppArmor stops it.

AppArmor vs SELinux Comparison
───────────────────────────────────────────────────────────
Feature AppArmor SELinux
───────────────────────────────────────────────────────────
Default distros Ubuntu, Debian, SUSE RHEL, Fedora, CentOS
Labeling Path-based (file paths) Type-based (labels)
Complexity Low (easier to learn) High (steeper curve)
Policy location /etc/apparmor.d/ SELinux modules
Debugging aa-logprof, /var/log/kern sealert, audit2allow
Coverage Per-application profiles System-wide TE policy
Portability Tied to file paths Works with network/IPC too
───────────────────────────────────────────────────────────
AppArmor Philosophy: "Confine programs by PATH"
SELinux Philosophy: "Confine by TYPE, regardless of path"

AppArmor Profile for nginx
───────────────────────────
/etc/apparmor.d/usr.sbin.nginx
#include <tunables/global>
/usr/sbin/nginx {
#include <abstractions/base>
#include <abstractions/nameservice>
capability net_bind_service, ← Can bind to port 80/443
capability setuid, ← Can change UID/GID
capability dac_override, ← Can bypass file perms
/var/www/html/** r, ← Read files in webroot
/etc/nginx/** r, ← Read nginx config
/var/log/nginx/** w, ← Write logs
/run/nginx.pid rw, ← Write PID file
/var/cache/nginx/** rw, ← Read/write cache
network tcp, ← Can use TCP
network udp, ← Can use UDP
deny /etc/shadow r, ← Explicitly deny shadow file
deny /root/** rwx, ← Deny access to root home
}
Everything not listed = DENIED (whitelist model)

Terminal window
# ── Check AppArmor Status ─────────────────────────────────
aa-status
# apparmor module is loaded.
# 45 profiles are loaded.
# 38 profiles are in enforce mode.
# /usr/sbin/nginx
# /usr/sbin/sshd
# ...
# 7 profiles are in complain mode.
# 0 processes have profiles defined.
# ── Profile Modes ─────────────────────────────────────────
# Enforce: Violations are blocked AND logged
# Complain: Violations are only logged (not blocked)
# Disabled: Profile not loaded
# Check mode of a specific profile
aa-status | grep nginx
# ── Change Profile Mode ───────────────────────────────────
aa-enforce /usr/sbin/nginx # Set to enforce
aa-complain /usr/sbin/nginx # Set to complain
aa-disable /usr/sbin/nginx # Disable profile
# Reload all profiles
systemctl reload apparmor # Reload all
apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx # Reload specific

Terminal window
# ── Where to find denials ─────────────────────────────────
# AppArmor logs to kernel ring buffer and syslog
dmesg | grep "apparmor"
journalctl -k | grep "apparmor.*DENIED"
cat /var/log/kern.log | grep "apparmor.*DENIED"
# Example denial:
# kernel: audit: type=1400 audit(1705312800.123:456):
# apparmor="DENIED" operation="open"
# profile="/usr/sbin/nginx" name="/var/lib/app/data.db"
# pid=1234 comm="nginx"
# requested_mask="r" denied_mask="r"
# fsuid=33 ouid=1000
# Breaking it down:
# profile="/usr/sbin/nginx" = which AppArmor profile denied it
# name="/var/lib/app/data.db" = what file was accessed
# requested_mask="r" = operation attempted (read)
# denied_mask="r" = what was denied
# ── aa-logprof: Interactive profile builder ───────────────
# Reads log file, suggests profile additions
aa-logprof
# Will show each denied access and ask what to do:
# (A)llow / (D)eny / (I)gnore / (G)lob

Terminal window
# ── Profile Syntax ────────────────────────────────────────
cat > /etc/apparmor.d/usr.bin.myapp << 'EOF'
#include <tunables/global>
/usr/bin/myapp {
# Include standard abstractions
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/ssl_certs>
# Capabilities
capability net_bind_service,
capability setgid,
capability setuid,
# File access (format: path access_mode)
# r = read, w = write, x = execute, a = append
# l = link, m = memory map, k = lock
/usr/bin/myapp mr, # Execute self (r=read, m=mmap exec)
/etc/myapp/** r, # Read config (all files under /etc/myapp/)
/var/lib/myapp/ r, # Read directory
/var/lib/myapp/** rw, # Read/write data files
/var/log/myapp/** w, # Write logs only (no read)
/var/log/myapp/ w, # Write to log directory
/run/myapp.pid rw, # PID file
/tmp/myapp-* rw, # Temp files with pattern
# Deny rules (override allows)
deny /etc/passwd r,
deny /etc/shadow r,
deny /root/** rwxl,
# Network access
network inet stream, # TCP IPv4
network inet6 stream, # TCP IPv6
network inet dgram, # UDP IPv4
# Unix sockets
/run/myapp.sock rw,
# Allow /proc access for self
/proc/@{pid}/** r, # @{pid} is a variable (current PID)
# Deny sensitive paths
deny /proc/*/mem rw,
deny /proc/sysrq-trigger rw,
deny /sys/kernel/security/** rwx,
}
EOF
# Load the new profile
apparmor_parser -r /etc/apparmor.d/usr.bin.myapp
# Verify it's loaded
aa-status | grep myapp

Abstractions are reusable profile snippets included with AppArmor:

Terminal window
# View available abstractions
ls /etc/apparmor.d/abstractions/
# Common abstractions:
# base - /proc/self/*, /dev/null, etc. (always include)
# nameservice - DNS, NSS lookups (/etc/hosts, /etc/resolv.conf)
# ssl_certs - SSL certificates (/etc/ssl/certs, CA paths)
# python - Python runtime libraries
# ruby - Ruby runtime
# bash - bash and common utilities
# Example: Python application needs these:
/usr/bin/python3-app {
#include <abstractions/base>
#include <abstractions/python>
#include <abstractions/nameservice>
...
}

Terminal window
# ── Step 1: Generate initial profile ─────────────────────
# Run the program once to generate a basic profile template
aa-genprof /usr/bin/myapp
# This puts the profile in complain mode and guides you through it
# ── Step 2: Exercise the application ─────────────────────
# In another terminal, run the application normally
# Do all normal operations: read config, write logs, serve requests
# ── Step 3: Update the profile ────────────────────────────
# Back in aa-genprof terminal, press 'S' to scan logs
# Approve/deny each suggestion
# ── Step 4: Switch to enforce mode ────────────────────────
aa-enforce /usr/bin/myapp
# ── Step 5: Monitor for issues ────────────────────────────
journalctl -k -f | grep "apparmor.*DENIED"
# ── Step 6: Use aa-logprof to update profile ──────────────
# If denials occur in enforce mode:
aa-logprof
# Review suggestions, approve as needed

Terminal window
# Docker automatically applies an AppArmor profile to containers
# Default profile: docker-default
docker inspect <container> | grep AppArmor
# Apply a custom AppArmor profile to a container
docker run --security-opt "apparmor=custom-profile" nginx
# View the default Docker AppArmor profile
cat /etc/apparmor.d/docker-default
# The docker-default profile:
# - Denies writes to /proc/*
# - Denies writes to /sys/**
# - Denies mount operations
# - Denies ptrace
# - Denies loading kernel modules

Scenario 1: nginx Cannot Serve Files from /srv/

Section titled “Scenario 1: nginx Cannot Serve Files from /srv/”
Terminal window
# Symptom: nginx returns 403 Forbidden for /srv/website/ files
# Nginx has correct DAC permissions but AppArmor blocks it
# Step 1: Check AppArmor denials
journalctl -k | grep "apparmor.*DENIED.*nginx" | tail -10
# apparmor="DENIED" operation="open" profile="/usr/sbin/nginx"
# name="/srv/website/index.html" requested_mask="r"
# Step 2: Find the nginx AppArmor profile
cat /etc/apparmor.d/usr.sbin.nginx
# Step 3: The profile only allows /var/www/ — need to add /srv/
# Option A: Use aa-logprof
aa-logprof
# Select allow for /srv/website/** r
# Option B: Manually edit the profile
sudo nano /etc/apparmor.d/usr.sbin.nginx
# Add inside the profile block:
# /srv/** r,
# Step 4: Reload profile
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
# Step 5: Verify nginx can now serve the files
curl http://localhost/index.html

Q1: What is the main conceptual difference between AppArmor and SELinux?

Answer: AppArmor is path-based: profiles define what a program can do based on file paths. If a file moves, the profile may no longer apply. AppArmor is generally easier to understand and configure. SELinux is label-based: every process and file gets a type label, and policy defines which types can interact. Labels persist regardless of file location. SELinux provides stronger security (a file relabeled as the wrong type is still caught), while AppArmor provides a lower barrier to entry. Ubuntu uses AppArmor by default; RHEL uses SELinux.

Q2: How would you add a new allowed path to an AppArmor profile for nginx?

Answer: (1) Put the profile in complain mode: aa-complain /usr/sbin/nginx. (2) Perform the operation that’s being denied. (3) Run aa-logprof to process the log and interactively approve the new access. Or (4) Manually edit /etc/apparmor.d/usr.sbin.nginx to add a rule like /new/path/** r,. (5) Reload the profile: apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx. (6) Return to enforce mode: aa-enforce /usr/sbin/nginx. (7) Verify the operation now works and no new denials appear in journalctl -k | grep apparmor.


ConceptKey Point
ProfileWhitelist of allowed paths and capabilities
Enforce modeViolations blocked and logged
Complain modeViolations only logged (for development)
aa-logprofInteractive profile updater from logs
aa-genprofProfile generator for new applications
AbstractionsReusable profile snippets

Chapter 14 (Security Fundamentals).


Advantages: Much easier to learn and use than SELinux, path-based rules are intuitive. Disadvantages: Less comprehensive than SELinux, relies on paths instead of inode labels.


  • Leaving profiles in complain mode in production.
  • Not updating profiles when application paths change.

SymptomCauseDiagnosisFix
App unexpectedly killed or denied accessAppArmor profile too restrictive`dmesggrep apparmor; grep DENIED /var/log/syslog`

Objective: Create and manage an AppArmor profile.

Terminal window
# 1. Check AppArmor status
aa-status
# 2. Put a profile in complain mode
aa-complain /usr/sbin/nginx
# 3. Generate a new profile using aa-genprof
# aa-genprof /path/to/custom/binary

  1. Write a simple bash script that reads /etc/passwd. Create an AppArmor profile that blocks this script from reading that file.
  2. Use aa-logprof to parse audit logs and update a profile in complain mode.

  • AppArmor uses path-based MAC (unlike SELinux’s label-based MAC).
  • Profiles can be in enforce mode (blocking) or complain mode (logging only).
  • Easier to learn and use than SELinux for many workloads.

  • Ubuntu AppArmor documentation
  • AppArmor upstream Wiki


Last Updated: July 2026