Privacy Tools Guide

How to Set Up AppArmor Profiles on Ubuntu

AppArmor restricts what a process can do by defining explicit rules about which files it can read, write, or execute, which network operations it can perform, and which capabilities it can use. When a program is compromised, AppArmor limits what the attacker can reach. Ubuntu ships with AppArmor enabled and includes profiles for common daemons — this guide covers writing custom profiles and enforcing them.

Understanding AppArmor Modes

AppArmor runs profiles in two modes:

# Check AppArmor status
sudo aa-status

# Typical output:
# 34 profiles are loaded.
# 32 profiles are in enforce mode.
# 2 profiles are in complain mode.
# 4 processes have profiles defined.

Tools You Need

sudo apt install apparmor-utils apparmor-profiles apparmor-profiles-extra

Key commands:

Step 1: Generate a Profile with aa-genprof

aa-genprof runs your application, watches what it does, and builds an initial profile.

# Example: create a profile for a custom Python script
sudo aa-genprof /usr/local/bin/myapp.py

aa-genprof will prompt you to run the application in another terminal:

# Terminal 2: run the application through its normal operations
python3 /usr/local/bin/myapp.py --config /etc/myapp/config.yaml

Back in terminal 1, press S to scan the log, then review each access and allow or deny it. At the end, press F to finish and save.

Step 2: Write a Profile Manually

Understanding profile syntax lets you write precise rules.

# /etc/apparmor.d/usr.local.bin.myapp
# AppArmor profile for myapp

#include <tunables/global>

/usr/local/bin/myapp.py {
  #include <abstractions/base>
  #include <abstractions/python>

  # Binary itself (read and execute)
  /usr/local/bin/myapp.py r,
  /usr/bin/python3* rix,

  # Config file (read only)
  /etc/myapp/config.yaml r,

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

  # Log file (append only)
  /var/log/myapp.log a,

  # PID file
  /run/myapp.pid rw,

  # Deny access to sensitive paths explicitly
  deny /etc/shadow r,
  deny /etc/passwd r,
  deny /root/** rw,
  deny /home/** rw,

  # Network: allow outbound TCP on port 443 only
  network inet stream,
  network inet6 stream,

  # Capabilities needed (list only what's required)
  # capability net_bind_service,   # only if binding to port < 1024
}

Permission flags:

Step 3: Profile for nginx

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

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

  # nginx binary
  /usr/sbin/nginx mr,

  # Configuration
  /etc/nginx/ r,
  /etc/nginx/** r,

  # Certificates
  /etc/ssl/certs/ r,
  /etc/ssl/certs/** r,
  /etc/letsencrypt/live/ r,
  /etc/letsencrypt/live/** r,
  /etc/letsencrypt/archive/ r,
  /etc/letsencrypt/archive/** r,

  # Web root
  /var/www/ r,
  /var/www/** r,

  # Logs
  /var/log/nginx/ r,
  /var/log/nginx/** rw,

  # Runtime
  /run/nginx.pid rw,
  /tmp/nginx-* rw,

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

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

  # Deny access to anything else
  deny /etc/shadow r,
  deny /proc/*/mem r,
  deny @{HOME}/** rw,
}

Step 4: Load and Enforce a Profile

# Parse and load a profile
sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.myapp

# Start in complain mode first
sudo aa-complain /usr/local/bin/myapp.py

# Run the application and check logs
sudo journalctl -f -k | grep apparmor &
/usr/local/bin/myapp.py --test-all-features

# Review what was denied
sudo cat /var/log/syslog | grep DENIED | tail -30

# Update profile from logs
sudo aa-logprof

# Once happy, enforce
sudo aa-enforce /usr/local/bin/myapp.py

# Reload
sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.myapp
sudo systemctl restart myapp

Step 5: Debugging Denials

# Watch for AppArmor denials in real time
sudo journalctl -f | grep 'apparmor="DENIED"'

# Typical denial log entry:
# kernel: audit: type=1400 audit(1711040000.123:456): apparmor="DENIED"
# operation="open" profile="/usr/local/bin/myapp.py"
# name="/etc/hosts" pid=12345 comm="python3"
# requested_mask="r" denied_mask="r" fsuid=1000 ouid=0

# Parse AppArmor audit logs with aa-logprof
sudo aa-logprof

# It reads /var/log/syslog or /var/log/audit/audit.log
# and presents each denial for you to allow/deny

When you see a denial that should be allowed, add the rule to the profile:

sudo nano /etc/apparmor.d/usr.local.bin.myapp
# Add the missing rule, e.g.: /etc/hosts r,
sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.myapp

Step 6: AppArmor and Docker

Docker containers can have AppArmor profiles applied:

# Run a container with a custom AppArmor profile
docker run --security-opt apparmor=docker-nginx-profile nginx

# List active container profiles
docker inspect my-container | jq '.[0].HostConfig.SecurityOpt'

Docker includes a default AppArmor profile (docker-default) that is applied automatically. You can inspect it:

cat /etc/apparmor.d/docker-default

To customize it for a specific service, copy and modify the default profile, then reference it by name.

Auditing Profiles with aa-status

# Full status
sudo aa-status

# Just the enforced profiles
sudo aa-status | grep "enforce mode" -A 50 | grep "   /"

# Check which processes are confined
sudo aa-status --json | jq '.processes | to_entries[] | {profile: .key, pids: .value}'