Introduction
By default on Linux, a process runs with the rights of the user who launched it: Nginx as www-data can read all of /etc, write to /tmp, open any port. If Nginx is compromised, the attacker has access to all that.
AppArmor restricts each process to what it actually needs via profiles that list allowed files and capabilities. If Nginx is compromised, the attacker can only access /var/www, read its certs and listen on 80/443. Nothing else.
Advantages vs SELinux: readable syntax, path-based, ships with Ubuntu/Debian, ready-made profiles for common services.
Prerequisites
- Ubuntu / Debian (AppArmor preinstalled)
- Root access
- Basic Linux CLI
Step 1: Check AppArmor is active
sudo systemctl status apparmor
sudo aa-status
Typical output:
apparmor module is loaded.
35 profiles are loaded.
30 profiles are in enforce mode.
5 profiles are in complain mode.
12 processes have profiles defined.
If not installed:
sudo apt install -y apparmor apparmor-utils apparmor-profiles
sudo systemctl enable --now apparmor
Step 2: AppArmor modes
3 modes per profile:
- enforce: blocks + logs (prod mode)
- complain: logs only, doesn't block (test mode)
- disabled: profile loaded but inactive
sudo aa-complain /etc/apparmor.d/usr.sbin.nginx
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
sudo aa-disable /etc/apparmor.d/usr.sbin.nginx
Step 3: List existing profiles
ls /etc/apparmor.d/
sudo apt install -y apparmor-profiles apparmor-profiles-extra
Step 4: Anatomy of a profile
#include <tunables/global>
/usr/sbin/nginx {
#include <abstractions/base>
capability net_bind_service,
capability setgid,
capability setuid,
/etc/nginx/** r,
/etc/ssl/certs/** r,
/var/log/nginx/*.log w,
/var/www/** r,
/run/nginx.pid w,
network inet stream,
network inet6 stream,
}
Legend: r read, w write, m mmap, k lock, x execute, ix inherit, Px exec with own profile.
Step 5: Create a profile for a new service
sudo aa-genprof /opt/downloader/app.py
In another terminal, run the script. AppArmor asks for each access.
Or manual:
sudo nano /etc/apparmor.d/opt.downloader.app
#include <tunables/global>
/opt/downloader/app.py {
#include <abstractions/base>
#include <abstractions/python>
#include <abstractions/openssl>
/usr/bin/python3 ix,
/opt/downloader/** r,
/srv/downloads/** rw,
network inet stream,
network inet6 stream,
}
sudo apparmor_parser -r /etc/apparmor.d/opt.downloader.app
sudo aa-enforce /etc/apparmor.d/opt.downloader.app
Step 6: Recommended workflow (complain → enforce)
sudo aa-complain /opt/downloader/app.py- Run the app normally for a few days
sudo aa-logprofto add missing permissionssudo aa-enforce /opt/downloader/app.py
Step 7: View logged violations
sudo dmesg | grep DENIED
sudo journalctl -k | grep apparmor
Example:
apparmor="DENIED" operation="open" profile="/opt/downloader/app.py" name="/etc/shadow"
Step 8: Profiles for Docker
sudo nano /etc/apparmor.d/docker-nginx
#include <tunables/global>
profile docker-nginx flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network inet tcp,
/etc/nginx/** r,
/var/log/nginx/** w,
/var/cache/nginx/** rw,
/var/www/** r,
deny /proc/sysrq-trigger w,
}
sudo apparmor_parser -r /etc/apparmor.d/docker-nginx
docker run --security-opt apparmor=docker-nginx nginx
Step 9: Real-world case — protect vsftpd
#include <tunables/global>
/usr/sbin/vsftpd {
#include <abstractions/base>
#include <abstractions/nameservice>
capability net_bind_service,
capability setuid,
capability setgid,
/etc/vsftpd.conf r,
/srv/ftp/** rw,
/var/log/vsftpd.log w,
network inet stream,
deny /etc/shadow r,
deny /root/** rwx,
deny /home/** rwx,
}
Even if vsftpd is compromised, attacker can't read /home, /root, or /etc/shadow.
Step 10: Common abstractions
In /etc/apparmor.d/abstractions/:
base: essential libc, DNSnameservice: DNS, /etc/hostspython: standard Python libsopenssl: cryptonginx: Nginx needsapache2-common: Apachessl_certs: read certs
Step 11: Reload after changes
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
sudo systemctl reload apparmor
Step 12: Disable AppArmor (avoid)
sudo systemctl stop apparmor
sudo systemctl disable apparmor
⚠️ Never leave disabled in prod on exposed services.
Troubleshooting
Service won't start after enforce
sudo dmesg | grep DENIED | tail -20
sudo aa-complain /etc/apparmor.d/usr.sbin.servicename
"Failed to apply profile"
sudo apparmor_parser -Q /etc/apparmor.d/usr.sbin.servicename
Profile doesn't apply
which nginx
# Profile must list the correct binary path
Useful commands
sudo aa-status # overall status
sudo aa-logprof # suggest rules from logs
ps auxZ | grep nginx # see if confined
sudo aa-unconfined # unconfined processes
sudo apparmor_parser -r /etc/apparmor.d/profile # reload one
sudo aa-mergeprof # merge DENIED into rules
Conclusion
AppArmor adds a major defense layer to your Linux:
- Per-binary confinement
- Complain mode to test without breaking
- Ready profiles for common services
- Combinable with Docker, systemd
Going further: combine with seccomp, use bpftrace for live observation, migrate to SELinux on RedHat / CentOS.
Resources
- Ubuntu docs: https://ubuntu.com/server/docs/security-apparmor
- AppArmor Wiki: https://gitlab.com/apparmor/apparmor/-/wikis/Documentation
- Community profiles: https://gitlab.com/apparmor/apparmor-profiles
- Syntax reference: https://manpages.ubuntu.com/manpages/jammy/man5/apparmor.d.5.html

















