Why Traditional chmod/chown Is No Longer Enough
File permissions (chmod, chown) serve as the first line of defense on Linux. In practice, however, they are simply not enough on their own.
Consider a common scenario: Nginx or a Python API is compromised via a Remote Code Execution (RCE) vulnerability. At this point, the attacker immediately assumes the full permissions of the www-data user. They can now inspect everything in /tmp, steal source code containing database credentials, or execute malicious binaries from /dev/shm.
To isolate processes and prevent privilege escalation, systems engineers typically consider three main solutions:
- Chroot Jail / Containers (Docker, Podman): Package applications into isolated namespaces and cgroups. While very convenient, container escapes can still happen if the kernel has vulnerabilities (such as Dirty COW) or if the container is mistakenly run with the
--privilegedflag. - SELinux (Security-Enhanced Linux): An extremely powerful Mandatory Access Control (MAC) mechanism based on security labels. However, its policy syntax is notoriously complex, where even a simple mount point migration can mysteriously break a service.
- AppArmor (Application Armor): A built-in MAC solution on Ubuntu, Debian, and openSUSE. AppArmor controls permissions directly based on file paths (path-based), making it much easier to read, debug, and manage.
When Should You Choose AppArmor?
If your server runs Ubuntu or Debian, AppArmor is the top choice for fast server hardening with minimal risk of downtime.
Specifically, AppArmor shines when you need to:
- Lock down a network process (such as a web server or a custom file upload script) so it can only read and write to designated directories.
- Prevent interactive shells (like
/bin/shor/bin/bash) from spawning if an attacker injects a web shell. - Provide a lightweight layer of defense for bare-metal services without the overhead of container orchestration.
Hands-on: Building an AppArmor Profile from Scratch
Step 1: Install Tools and Verify Kernel Support
Most Ubuntu LTS releases (from 20.04 to 24.04) come with AppArmor enabled in the kernel by default. You only need to install the management utilities:
sudo apt update
sudo apt install -y apparmor-utils apparmor-profiles
# Check the current system status
sudo aa-status
The aa-status command displays the number of profiles currently in enforce mode (actively blocking violations) and complain mode (logging warnings without blocking execution).
Step 2: Create a Sample Python Service to Protect
For demonstration purposes, let’s create a Python script designed to process files at /opt/myapp/app.py. This script only requires two legitimate permissions: appending logs to /var/log/myapp.log and saving files to /var/data/uploads/.
sudo mkdir -p /opt/myapp /var/data/uploads
sudo bash -c 'cat << "EOF" > /opt/myapp/app.py
#!/usr/bin/python3
import os
# 1. Legitimate task: Log activity
with open("/var/log/myapp.log", "a") as f:
f.write("Service running normally.\n")
# 2. Malicious task: Simulate an attacker trying to read sensitive system files
try:
with open("/etc/shadow", "r") as f:
print("[WARNING] Successfully snooped /etc/shadow!")
except Exception as e:
print(f"[SAFE] Access to /etc/shadow failed: {e}")
EOF'
sudo chmod +x /opt/myapp/app.py
If you run /opt/myapp/app.py as root, you will see that the script reads /etc/shadow without issue. Now, we will use AppArmor to stop this behavior.
Step 3: Write a Custom Profile with an Explicit Whitelist
Profile filenames inside /etc/apparmor.d/ map path slashes / to dots .. Create the file /etc/apparmor.d/opt.myapp.app.py:
sudo bash -c 'cat << "EOF" > /etc/apparmor.d/opt.myapp.app.py
#include <tunables/global>
/opt/myapp/app.py {
#include <abstractions/base>
#include <abstractions/python>
# Allow execution of the Python interpreter, inheriting the profile
/usr/bin/python3* ix,
# Permissions to read source code and read/write the data directory
/opt/myapp/ r,
/opt/myapp/** r,
/var/data/uploads/ rw,
/var/data/uploads/** rw,
/var/log/myapp.log rwk,
# Explicitly deny spawning child shells and accessing sensitive files
deny /bin/** mrwklx,
deny /usr/bin/** mrwklx,
deny /etc/shadow* mrwklx,
deny /etc/passwd* mrwklx,
}
EOF'
Key permission flags to remember:
r(Read): Read file contents or list directories.w(Write): Create or overwrite files.k(Locking): Allow the application to acquire file locks during thread execution.ix(Inherit Execute): Execute child processes under this same profile.deny: Explicitly deny access; always takes precedence over allow rules.
Step 4: Load the Profile and Monitor in Complain Mode
A golden rule for production: Never enable enforce mode immediately. Let the profile run in complain mode for at least a few days to capture all real-world behavior:
# Load the new profile into the kernel
sudo apparmor_parser -r /etc/apparmor.d/opt.myapp.app.py
# Put the profile into monitoring (complain) mode
sudo aa-complain /opt/myapp/app.py
In real-world deployments, applications often load shared libraries (.so) or hidden configuration files that developers might miss during initial implementation. Enabling enforce mode right away will instantly trigger Permission Denied errors. Complain mode ensures the application continues running normally while logging all violations to the system log so you can append missing permissions.
Step 5: Audit Logs and Activate Enforce Mode
After testing the complete application workflow, inspect the recorded AppArmor logs:
# Filter AppArmor violation logs from the systemd journal
sudo journalctl -fx | grep -i apparmor
# Use the interactive utility to scan logs and automatically update missing rules
sudo aa-logprof
# Once you are confident all legitimate rules are present, switch to active enforcement
sudo aa-enforce /opt/myapp/app.py
Now, rerun the script with sudo /opt/myapp/app.py. The output will show:
[SAFE] Access to /etc/shadow failed: [Errno 13] Permission denied: '/etc/shadow'
Even though it runs as root, the process is denied access to /etc/shadow by the kernel because this action is not whitelisted in the profile.

