Architectural Foundation of Linux Mandatory Access Control and AppArmor
Traditional Linux security relies on Discretionary Access Control (DAC), governed by standard file permissions (read, write, execute) and POSIX ownership (user, group, others). Under pure DAC, if a compromised system service runs as root or an application user possesses wide read permissions, an attacker can pivot across the filesystem, inspect private SSH host keys, or inject malicious binaries into unconfined directories. To neutralize this privilege escalation risk, Ubuntu Server implements AppArmor (Application Armor), a kernel-level Mandatory Access Control (MAC) subsystem built upon the Linux Security Modules (LSM) framework.
Unlike SELinux, which binds access policies to inode filesystem labels, AppArmor is path-based. Security profiles in AppArmor are directly attached to executable file paths (e.g., /usr/sbin/nginx or /usr/bin/evince). Whenever a process invokes a system call to access the filesystem, bind a network socket, acquire POSIX capabilities, or spawn a child thread, the AppArmor kernel module intercepts the operation before file descriptor allocation. If the requested interaction is not explicitly authorized within the active profile manifest, the kernel terminates the call immediately with an EACCES (Permission Denied) error and dispatches a structured audit security event to the kernel ring buffer.
| Operational Mode | Kernel Enforcement Action | System Audit Logging | Primary Administrative Function |
|---|---|---|---|
| Enforce Mode | Strictly blocks unauthorized system calls | Logs violations as apparmor="DENIED" |
Production lockdown; active threat containment |
| Complain Mode | Permits execution; does not block calls | Logs violations as apparmor="ALLOWED" |
Profile development, staging, behavioural learning |
| Disabled | Profile detached from kernel | Zero AppArmor logging | Diagnostic isolation during debugging |
| Unconfined | Process executes under standard DAC only | No restrictions applied | Default state for non-profiled binaries |
Auditing AppArmor Status on Ubuntu Server
To inspect active kernel profiles, loaded rulesets, and running processes confined by AppArmor, use the native aa-status command:
# Audit active AppArmor status and profile confinement metrics
sudo aa-status
The command outputs a comprehensive status breakdown:
apparmor module is loaded.
42 profiles are loaded.
38 profiles are in enforce mode.
4 profiles are in complain mode.
12 processes have profiles defined.
11 processes are in enforce mode.
1 processes are in complain mode.
0 processes are unconfined but have a profile defined.
Switching Between Enforce and Complain Modes
When developing a new profile or troubleshooting service failures following an application update, transition the target profile between Complain and Enforce states using the apparmor-utils toolchain:
# Install AppArmor administrative utilities if not present
sudo apt update && sudo apt install -y apparmor-utils apparmor-profiles
# 1. Place a profile into Complain (learning) mode
sudo aa-complain /etc/apparmor.d/usr.sbin.nginx
# 2. Return the profile to strict Enforce mode
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
# 3. Disable a profile completely without deleting the policy file
sudo aa-disable /etc/apparmor.d/usr.sbin.nginx
Constructing a Hardened Custom Security Profile
AppArmor profiles reside within /etc/apparmor.d/ and follow a structured declarative grammar defining filesystem permissions, network privileges, and capabilities.
File Permission Flags in AppArmor:
r: Read data.w: Write data or truncate existing files.a: Append-only write (ideal for log files).m: Memory map executable (PROT_EXEC).k: File lock locking permission.ix: Execute inheriting current profile rules.px: Execute transitioning to a discrete discrete child profile.
Step 1: Write a Declarative Profile for an Internal Web Service
Create a hardened profile for a custom Python/Node.js web API residing at /opt/custom_api/server.py:
sudo tee /etc/apparmor.d/opt.custom_api.server.py << 'EOF'
#include
/opt/custom_api/server.py {
#include
#include
#include
# Network privileges
network inet stream,
network inet6 stream,
# Bound capabilities: allow binding to privileged ports (<1024)
capability net_bind_service,
# Application file assets: Read-only access to code
/opt/custom_api/ r,
/opt/custom_api/** r,
# Log files: Append-only access
/var/log/custom_api/*.log wka,
# Temporary working files
/tmp/api_cache/** rw,
# Explicit deny: block reading sensitive host credential files
deny /etc/shadow r,
deny /etc/ssh/* r,
deny /root/** rwx,
}
EOF
Step 2: Parse and Load the Profile into the Linux Kernel
# Parse and compile the profile directly into kernel LSM memory
sudo apparmor_parser -r -W /etc/apparmor.d/opt.custom_api.server.py
# Verify the newly loaded profile is enforcing
sudo aa-status | grep "opt.custom_api.server.py"
Interactive Learning with aa-genprof and aa-logprof
Rather than hand-crafting hundreds of library paths, administrators can profile unknown applications dynamically using aa-genprof:
# 1. Initialize profiling on a target binary
sudo aa-genprof /usr/local/bin/legacy_exporter
# 2. In a separate terminal, exercise all application features (API calls, file exports)
# 3. Return to aa-genprof and press 'S' to scan system audit logs
# 4. For each detected file access, accept, modify, or deny the suggested rule
# 5. Press 'F' to finish and commit the compiled profile into /etc/apparmor.d/
Troubleshooting AppArmor Denials in Audit Telemetry
| Observed Fault | Underlying Root Cause | Diagnostic Verification Method | Remediation Protocol |
|---|---|---|---|
| "Permission Denied" Despite 777 File Permissions | AppArmor actively blocking path access; MAC policy overrides standard DAC permissions | Inspect kernel audit log: sudo dmesg | grep -i "apparmor=\"DENIED\"" |
Add the required path and access mode (e.g., /var/www/uploads/ rw,) to the profile |
| Service Fails to Start with apparmor_parser Syntax Error | Unclosed brace, missing comma, or invalid regex path expression in profile | Run validation: sudo apparmor_parser -d /etc/apparmor.d/target.profile |
Correct formatting errors identified at the specific line number in the parser output |
| Audit Log Flooded with Non-Critical Denials | Application testing benign paths (e.g., probing for desktop environments or GUI libraries) | /var/log/syslog grows rapidly with repeated denial lines |
Add explicit quiet deny rules: deny /etc/xdg/** r, to suppress log generation |
| Containerised Application Unconfined in Docker | Docker host disabled default AppArmor profile or container run with --privileged |
Inspect container state: docker inspect [id] | grep AppArmorProfile |
Apply the default container profile: docker run --security-opt apparmor=docker-default |