Every Ubuntu VPS ships with AppArmor enabled, and almost nobody looks at it until a service starts failing with cryptic permission denied errors. AppArmor is a Mandatory Access Control (MAC) layer that restricts what programs can do even when they run as root — which makes it one of the few security controls that actually contains a compromised web app. The catch: default profiles are tuned for stock installs, and the moment you customize nginx, move a MySQL datadir, or add PAM modules to sshd, you start collecting denials. Here is the workflow that keeps your services running while the profiles still do their job.
Step 1: See what is actually loaded
sudo aa-status
# shows loaded profiles and their mode: enforce or complain
Profiles in enforce mode block denied operations. Profiles in complain mode only log them. On a fresh Ubuntu image, sshd is typically in complain mode and a handful of other services ship with profiles you never asked for. Knowing which mode each profile is in is the difference between a quiet weekend and a 2 a.m. support ticket.
Step 2: The workflow — complain, read logs, enforce
Never flip a profile straight to enforce after a config change. The repeatable loop is:
sudo aa-complain /etc/apparmor.d/usr.sbin.mysqld # 1. put the profile in complain mode
sudo systemctl restart mysql # 2. exercise the new config
sudo journalctl -k --since "10 min ago" | grep -i apparmor # 3. read the denials
sudo aa-logprof # 4. approve/deny the suggested rules
sudo aa-enforce /etc/apparmor.d/usr.sbin.mysqld # 5. only now enforce
Kernel denials also land in /var/log/syslog and dmesg. A typical denial looks like:
audit: type=1400 audit(1750000000.123:456) apparmor="DENIED"
operation="open" profile="/usr/sbin/nginx"
name="/srv/www/index.html" comm="nginx" requested_mask="r"
Read it left to right: which profile, which operation, which file, which access mask. That tells you exactly what rule to add.
Case 1: nginx with custom paths
The stock nginx profile allows /etc/nginx, /var/www, /var/log/nginx and the pid file. Serve sites from /srv/www or /home/deploy/sites and you will see exactly the denial above. Two ways out:
- Add rules with aa-logprof — the interactive tool proposes file rules from the denials; approve the ones that match paths you intend nginx to read.
- Append rules by hand — add to the profile file and reload:
# inside /etc/apparmor.d/usr.sbin.nginx
/srv/www/ r,
/srv/www/** r,
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
If you run nginx with a PHP-FPM socket, remember the socket path too — /run/php-fpm.sock usually needs an explicit rw rule.
Case 2: MySQL/MariaDB with a moved datadir
Moving /var/lib/mysql to a separate data volume is common on small VPSes, and the shipped profile (usr.sbin.mysqld) only knows the default path. The denials show up in the MySQL error log as failed file opens. Add the new location and reload the profile:
# inside /etc/apparmor.d/usr.sbin.mysqld
/data/mysql/ r,
/data/mysql/** rwk,
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.mysqld
Notice the k in the mask — MySQL needs lock permission on its files, and omitting it produces denials that look identical to filesystem permission problems. This is the single most common AppArmor debugging trap on database servers.
Case 3: sshd plus 2FA or custom auth
Ubuntu ships the sshd profile in complain mode by default, which is why SSH almost never breaks when you add TOTP via libpam-google-authenticator or a custom AuthorizedKeysCommand — it is logging, not blocking. If you decide to enforce the profile, expect denials from the new PAM modules and key-helper binaries, and work through them with the same complain → log → enforce loop. If enforcing sshd gains you little and costs support time, leaving it in complain mode while enforcing profiles for your web-facing services is a reasonable trade.
The recovery plan
If a service stops working after a profile change, you still have the provider’s console — AppArmor cannot stop you from logging in over the web console, and it does not modify files. The safe order is: sudo aa-disable /etc/apparmor.d/<profile>, restart the service, get it back up, then investigate the denials and re-enable with proper rules. What you should never do is chmod 644 or delete profile files across the board to make the errors go away — you are disabling the control, not fixing the policy.
A note on SELinux
RHEL-family images (CentOS, AlmaLinux, Rocky) use SELinux, which is the same idea with a different vocabulary: policy is label-based instead of path-based, and denials land in /var/log/audit/audit.log where audit2why translates them. If your provider’s default image is Ubuntu or Debian, AppArmor is the MAC you actually have, so this guide applies directly. Distro defaults are one of the things to check before you pick a provider — our VPS comparison table tracks which images each provider offers out of the box.
Is MAC worth the effort on a single VPS? Compared with SSH keys, a firewall, and unattended upgrades, AppArmor is a smaller increment — but it is the only one of those that contains a compromised nginx or PHP process. For a web server that touches customer data, spending an afternoon building one good profile for the app layer is cheap insurance.

Leave a Reply
You must be logged in to post a comment.