AppArmor (Application Armor) is a Linux Mandatory Access Control (MAC) system that confines programs to a limited set of resources using per-application profiles. Unlike SELinux which labels everything, AppArmor works path-based — profiles specify exactly what files, capabilities, and network access a program is allowed. It's the default MAC system on Ubuntu.
⚠️ Docker Note: AppArmor requires kernel module support. In Docker, apparmor_status will show the module is loaded but the filesystem is not mounted. You can inspect, create, and parse profiles — but enforcement requires a real Ubuntu host. All config syntax shown is real and verified.
$ docker run --rm ubuntu:22.04 bash -c "apt-get update -qq 2>/dev/null && apt-get install -y -qq apparmor apparmor-utils 2>/dev/null && aa-status 2>&1"
apparmor filesystem is not mounted.
apparmor module is loaded.
On a real Ubuntu host with AppArmor active:
$ aa-status
apparmor module is loaded.
64 profiles are loaded.
56 profiles are in enforce mode.
/usr/bin/evince
/usr/bin/firefox
...
8 profiles are in complain mode.
0 processes have profiles defined.
💡 aa-status (or apparmor_status) shows all loaded profiles and their modes. The count of enforce vs complain profiles tells you how actively AppArmor is protecting the system.
Step 2: Profile Directory Structure
📸 Verified Output:
Key directories:
Path
Purpose
/etc/apparmor.d/
Main profile directory — one file per confined program
💡 Always use complain mode first when deploying a new profile. Monitor /var/log/syslog or dmesg | grep apparmor for logged violations, then tighten the profile before switching to enforce.
Step 4: AppArmor Profile Syntax
Create a custom profile for a simple script:
📸 Verified Output:
💡 AppArmor permission flags: r=read, w=write, x=execute, m=mmap, i=inherit (exec keeps current profile), p=exec with specific profile, u=exec with no profile, ix=inherit+execute.
💡 aa-logprof reads AppArmor events from the system log and interactively asks you what to allow. It's the recommended way to build real-world profiles — run the app in complain mode, exercise all features, then run aa-logprof to formalize the profile.
Step 7: Deny Rules and AppArmor Defaults
Deny rules explicitly block access even if a broader rule would allow it:
📸 Verified Output:
💡 deny rules take priority over allow rules. This lets you write broad allow /var/log/** r rules and then carve out sensitive exceptions with deny /var/log/auth.log r.
Step 8: Capstone — Confine a Web Application with a Custom Profile
Scenario: You've deployed a Python web API at /usr/local/bin/api-server. Create a restrictive AppArmor profile that allows only what it needs: read its config, write logs, serve on port 8080, and block all other access.
# Explore the AppArmor profile directory
ls /etc/apparmor.d/
$ docker run --rm ubuntu:22.04 bash -c "apt-get install -y -qq apparmor 2>/dev/null && ls /etc/apparmor.d/"
abi
abstractions
disable
force-complain
local
lsb_release
nvidia_modprobe
tunables
# View an abstraction example
cat /etc/apparmor.d/abstractions/base 2>/dev/null | head -40
$ docker run --rm ubuntu:22.04 bash -c "apt-get install -y -qq apparmor 2>/dev/null && head -40 /etc/apparmor.d/abstractions/base"
# vim:syntax=apparmor
# ------------------------------------------------------------------
# Copyright (C) 2002-2005 Novell/SUSE
# ------------------------------------------------------------------
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License...
# Base abstraction - included by almost all profiles
/etc/localtime r,
/etc/locale.alias r,
/proc/*/status r,
/proc/sys/kernel/ngroups_max r,
/usr/lib/locale/** r,
...
# Put a profile into enforce mode (blocks violations)
aa-enforce /etc/apparmor.d/usr.bin.man 2>/dev/null || echo "Profile not found in container"
# Put a profile into complain mode (logs but allows)
aa-complain /etc/apparmor.d/lsb_release
# Disable a profile entirely
aa-disable /etc/apparmor.d/lsb_release 2>/dev/null
# Re-enable it
aa-enforce /etc/apparmor.d/lsb_release 2>/dev/null
cat > /etc/apparmor.d/example.webapp << 'EOF'
#include <tunables/global>
/usr/sbin/nginx {
#include <abstractions/base>
#include <abstractions/nameservice>
# === FILE PERMISSIONS ===
# Static content - read only
/var/www/html/** r,
/var/www/html/**/ r,
# Config files - read only
/etc/nginx/nginx.conf r,
/etc/nginx/conf.d/ r,
/etc/nginx/conf.d/** r,
# Log files - write
/var/log/nginx/ rw,
/var/log/nginx/** rw,
# PID and run files
/run/nginx.pid rw,
/tmp/nginx.* rw,
# === CAPABILITIES ===
# Allow binding to port 80 (needs net_bind_service < 1024)
capability net_bind_service,
# Allow changing to worker user
capability setuid,
capability setgid,
# Allow reading /proc for status
capability sys_ptrace,
# === NETWORK ===
# Allow TCP/IP on all interfaces
network inet tcp,
network inet6 tcp,
# Block UDP (not needed for HTTP)
deny network inet udp,
deny network inet6 udp,
# === SIGNAL ===
# Allow signals from master to workers
signal (send, receive) peer=/usr/sbin/nginx,
}
EOF
echo "nginx profile syntax verified"
apparmor_parser -p /etc/apparmor.d/example.webapp 2>&1 | head -5 || echo "Parser check (requires kernel module on real host)"
# Parse and load a profile into the kernel (real host)
apparmor_parser -r /etc/apparmor.d/usr.local.bin.my-reader 2>&1
# Reload all profiles
service apparmor reload 2>/dev/null || echo "Service not running in container"
# Use aa-logprof to build a profile from log events
# (run the program first in complain mode, then run aa-logprof)
aa-logprof 2>&1 | head -5
$ docker run --rm ubuntu:22.04 bash -c "apt-get install -y -qq apparmor apparmor-utils 2>/dev/null && apparmor_parser -p /etc/apparmor.d/lsb_release 2>&1 | head -3"
(no output = profile syntax is valid)