AppArmor Profiles for Custom Applications: From Complain Mode to Enforce
Problem
AppArmor is the default mandatory access control system on Ubuntu and Debian. It restricts applications to specific file paths, capabilities, and network access. Most applications (including custom ones) run without an AppArmor profile, meaning they have unrestricted access to anything the Unix user can reach.
Creating custom profiles requires iterative profiling that can break applications if done carelessly. The workflow (complain → observe → refine → enforce) takes 4-8 hours per application but provides confinement that survives application compromises.
Target systems: Ubuntu 24.04 LTS, Debian 12+. AppArmor is also available in Kubernetes via pod annotations.
Threat Model
- Adversary: Attacker with code execution inside a service. AppArmor confines the compromised service to specific paths and capabilities regardless of Unix permissions.
Configuration
Generating an Initial Profile
# Install AppArmor utilities
sudo apt install apparmor-utils
# Generate a profile for a custom application
sudo aa-genprof /opt/myapp/bin/myapp
# This starts the application in complain mode and monitors for access requests.
# Follow the interactive prompts to allow/deny each access.
# Alternative: generate from existing log data
sudo aa-logprof
# Reads AppArmor logs and suggests profile additions.
Manual Profile Writing
# /etc/apparmor.d/opt.myapp.bin.myapp
# AppArmor profile for custom web application
#include <tunables/global>
/opt/myapp/bin/myapp {
#include <abstractions/base>
#include <abstractions/nameservice>
# Binary itself
/opt/myapp/bin/myapp mr,
# Configuration files (read-only)
/opt/myapp/config/ r,
/opt/myapp/config/** r,
# Data directory (read-write)
/opt/myapp/data/ rw,
/opt/myapp/data/** rw,
# Log files (write, append, create)
/var/log/myapp/ rw,
/var/log/myapp/** w,
# Temporary files
/tmp/myapp-* rw,
# Shared libraries
/usr/lib/** mr,
/lib/** mr,
# Network: allow TCP on port 8080
network inet stream,
network inet6 stream,
# DNS resolution
/etc/resolv.conf r,
/etc/nsswitch.conf r,
/etc/hosts r,
# Deny everything else implicitly
# AppArmor denies any access not explicitly allowed.
}
Complain → Enforce Workflow
# Step 1: Load profile in complain mode (log but don't block)
sudo apparmor_parser -r -C /etc/apparmor.d/opt.myapp.bin.myapp
sudo aa-complain /opt/myapp/bin/myapp
# Step 2: Exercise the application (1-2 weeks of normal usage)
# All denied accesses are logged but allowed.
# Step 3: Review denials and update profile
sudo aa-logprof
# Interactive: for each denial, choose Allow, Deny, or Glob.
# Step 4: Switch to enforce mode
sudo aa-enforce /opt/myapp/bin/myapp
# Step 5: Monitor for violations
sudo dmesg | grep apparmor
# Look for DENIED entries - these indicate the profile is too restrictive.
Kubernetes Integration
# Pod with AppArmor profile
apiVersion: v1
kind: Pod
metadata:
name: myapp
annotations:
container.apparmor.security.beta.kubernetes.io/myapp: localhost/myapp-profile
spec:
containers:
- name: myapp
image: registry.example.com/myapp:v1
# Load the profile on all nodes:
# Deploy via DaemonSet that copies profiles to each node
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: apparmor-loader
namespace: kube-system
spec:
selector:
matchLabels:
app: apparmor-loader
template:
metadata:
labels:
app: apparmor-loader
spec:
containers:
- name: apparmor-loader
image: registry.example.com/apparmor-loader:v1
command: ["sh", "-c", "cp /profiles/* /etc/apparmor.d/ && apparmor_parser -r /etc/apparmor.d/myapp-profile && sleep infinity"]
volumeMounts:
- name: apparmor-dir
mountPath: /etc/apparmor.d
- name: profiles
mountPath: /profiles
volumes:
- name: apparmor-dir
hostPath:
path: /etc/apparmor.d
- name: profiles
configMap:
name: apparmor-profiles
EOF
Monitoring Violations
# Check for AppArmor denials in kernel log
sudo dmesg | grep "apparmor=\"DENIED\""
# Parse denials for monitoring
sudo journalctl -k | grep "apparmor=\"DENIED\"" | \
awk '{for(i=1;i<=NF;i++) if($i ~ /profile=|operation=|name=/) print $i}'
# Prometheus: export AppArmor denial count via node_exporter textfile
echo "apparmor_denied_total $(dmesg | grep -c 'apparmor=\"DENIED\"')" > \
/var/lib/node_exporter/apparmor.prom
Expected Behaviour
aa-statusshows the application profile in enforce modedmesg | grep apparmorshows zero DENIED entries during normal operation- Application runs normally with full functionality
- Any access outside the profile is blocked and logged
Trade-offs
| Control | Impact | Risk | Mitigation |
|---|---|---|---|
| Complain mode (1-2 weeks) | No security enforcement during learning | Attacks during complain mode are unconfined | Use complain mode in staging, not production. Switch to enforce before production deployment. |
| Path-based profiles | Easy to write and understand | New file paths from application updates break the profile | Update profiles as part of the deployment pipeline. Test in complain mode after updates. |
| Kubernetes AppArmor | Per-container confinement | Profile must be loaded on every node | DaemonSet-based profile loader ensures all nodes have the profile. |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Profile too restrictive | Application crashes or returns errors | dmesg shows DENIED entries; application logs show “Permission denied” |
Switch to complain mode. Identify missing paths. Update profile. Re-enforce. |
| Profile not loaded on node | Pod fails to start with AppArmor error | kubectl describe pod shows AppArmor profile not found |
Verify DaemonSet-based profile loader. Check profile exists on the node. |
| Application update adds new paths | Feature breaks after update (profile blocks new file access) | DENIED entries in dmesg after application update | Update profile to include new paths. Test in complain mode first. |
When to Consider a Managed Alternative
Profile creation takes 4-8 hours per application. Maintaining profiles across application updates requires ongoing effort.
- Sysdig (#122) and Aqua (#123): Provide automated profile generation from observed behaviour. Managed runtime enforcement handles MAC-equivalent confinement for containers.
- Managed Kubernetes: Providers handle node-level AppArmor. Container workloads use seccomp and capabilities (Article #91) which are simpler to manage.
Premium content pack: AppArmor profile collection for common applications (nginx, postgresql, redis, node, python, go) with Kubernetes deployment configurations and a profile update workflow guide.