AppArmor hardening

← Back

I'm a little freaked out about security, especially with the rise of ML scanning tools that make it possible to detect critical vulnerabilities and take advantage of them before anyone notices.

Of course there are good people who share their findings with the developers. But first, some companies steal money from bug hunters...

AppArmor

Install AppArmor

sudo apt install apparmor apparmor-utils apparmor-profiles

Check service status

systemctl status apparmor

Check AppArmor profiles

sudo apparmor_status

Start generate config

this saves time and potential breakage, tried to create php profile manually, took quite a time and broke the site a couple of times

sudo aa-genprof /usr/sbin/nginx

In another terminal

sudo service nginx restart

Start using the web as usual

Afterwards press S to scan and by doing that you will be prompted several times to allow or deny a capability.

Go step by step and read every request.

At the end you will be asked to save profile. You will need to perform couple more actions like stopping, starting, reloading of Nginx to bind access control attributes to selected program.

Generally look at severity, write/execute access, syscalls, sockets, raw udp, dac overrides

r - read
w - write
mr - memory read
x - execute
ix - execute inheriting profile (executed program continues running under caller's profile)
px - execute with profile transition (executes a program using a profile that matches the executed binary's name; denied otherwise)
Px - execute with profile transition (executes a program using a profile that matches the executed binary's name; denied otherwise + deletes environment variables)
ux - execute unconfined (executed program runs unconfined)
Ux - execute unconfined (executed program runs unconfined + deletes environment variables)
k - file lock

Set to complain mode

sudo aa-complain /usr/sbin/nginx

Enforce

sudo aa-enforce /usr/sbin/nginx

Check AppArmor status

sudo aa-status

Reload AppArmor

systemctl reload apparmor

Open the profile

nano /etc/apparmor.d/usr.sbin.nginx/

It would looks something like this

abi <abi/5.0>,

include <tunables/global>

/usr/sbin/nginx flags=(complain) {
  include <abstractions/apache2-common>
  include <abstractions/base>
  include <abstractions/dovecot-common>
  include <abstractions/nis>
  include <abstractions/postfix-common>
  include <abstractions/ssl_certs>
  include <abstractions/ssl_keys>

  /usr/sbin/nginx mr,
  /var/log/nginx/access.log w,
  /var/log/nginx/error.log w,
}

Which can be relaxed to

# Last Modified: Tue May  5 14:03:26 2026
abi <abi/5.0>,

include <tunables/global>

/usr/sbin/nginx {
  include <abstractions/apache2-common>
  include <abstractions/base>
}

Reload profile after manual changes

# Load a new profile in complain mode
sudo apparmor_parser -C /etc/apparmor.d/profile.name

# Replace existing profile
sudo apparmor_parser -r /etc/apparmor.d/profile.name

# Remove profile
sudo apparmor_parser -R /etc/apparmor.d/profile.name
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx

Grep denial logs

sudo journalctl -k | grep -i "apparmor"

sudo journalctl -k | grep "apparmor.*DENIED"

sudo journalctl -k -f | grep "apparmor"

Also can view logs via apparmor-notify

sudo apt install apparmor-notify
sudo aa-notify -s 1 -v

Or auditd

cat /var/log/audit/audit.log | grep "apparmor" | grep "DENIED"
type=AVC msg=audit(1778046109.376:2290): apparmor="DENIED" operation="exec" class="file" profile="php-fpm" name="/usr/bin/dash" pid=2391 comm="php-fpm8.5" requested_mask="x" denied_mask="x" fsuid=33 ouid=0FSUID="www-data" OUID="root"

If something isn't accessible then edit the profile to allow...

DONT FORGET TO DISABLE CACHING IN THE BROWSER WHILE DEBUGGING

One time nginx has requested a dac_override capability...

audit: type=1400 audit(1777994442.254:1613): apparmor="DENIED" operation="capable" class="cap" profile="/usr/sbin/nginx" pid=13164 comm="nginx" capability=1  capname="dac_override"

If start fails read the logs

journalctl -xeu nginx.service

Now I get why it needs dac_override, it's to read logs

open() "/var/log/nginx/error.log" failed (13: Permission denied)

Most blogs just give a dac_override capability...

chown -R root:adm /var/log/nginx
/var/log/nginx/*.log {
  daily
  missingok
  rotate 14
  compress
  delaycompress
  notifempty
  create 0640 root adm
  sharedscripts
  prerotate
    if [ -d /etc/logrotate.d/httpd-prerotate ]; then
      run-parts /etc/logrotate.d/httpd-prerotate;
    fi
  endscript
  postrotate
    invoke-rc.d nginx rotate >/dev/null 2>&1
  endscript
}
sudo aa-complain /etc/apparmor.d/php-fpm
2026/05/06 04:13:15 [error] 2375#2375: *15 FastCGI sent in stderr: "PHP message: PHP Warning:  PHP Request Startup: Failed to open stream: Permission denied in Unknown on line 0; Unable to open primary script: /var/www/r1nge.com/flatpress-1.4.1/index.php (Permission denied)" while reading response header from upstream, client: 127.0.0.1, server: r1nge.com, request: "GET / HTTP/1.1", upstream: "fastcgi://unix:/var/run/php/php8.5-fpm.sock:", host: "r1nge.com"

Fix ownership & permissions (assumes php-fpm runs as www-data)

sudo chown -R www-data:www-data /var/www/r1nge.com
sudo find /var/www/r1nge.com -type d -exec chmod 755 {} \;
sudo find /var/www/r1nge.com -type f -exec chmod 644 {} \;

PHP

# vim: ft=apparmor
abi <abi/5.0>,
include <tunables/global>
profile php-fpm /usr/{bin,sbin}/php-fpm* flags=(attach_disconnected) {  # load my website  owner /var/www/* rw,  /tmp rw,  /tmp/* rw,  /tmp/** rw,
  # allow executing timeout and locale  /usr/bin/timeout Px,  /usr/bin/locale Px,  /usr/lib/locale/** r,  /var/www/r1nge.com/** rw,  /usr/bin/timeout Px,  /usr/bin/locale Px,  /usr/lib/locale/** r,  /tmp/** rw,
  # execute dash  /usr/bin/dash Px,
  # locking  /var/www/r1nge.com/flatpress-1.4.1/** k,
  # load common libraries and their support files  include <abstractions/base>
  # resolve hostnames/usernames  include <abstractions/nameservice>
  # common php files and support files that php needs  include <abstractions/php>
  # read the system certificates  include <abstractions/ssl_certs>
  capability net_admin,  capability setuid,  capability setgid,  capability chown,  capability kill,  capability dac_override,
  @{PROC}/@{pid}/attr/{apparmor/,}current rw,
  # the main log file  /var/log/php*-fpm.log rw,
  # we need to be able to create all sockets  @{run}/php{,-fpm,-fpm-legacy}/php*-fpm.pid rw,  @{run}/php*-fpm.pid rw,  @{run}/php{,-fpm,-fpm-legacy}/*.sock{,et} rwlk,
  # LP: #2061113  owner @{run}/systemd/notify w,
  # to reload  @{exec_path} rix,
  # no idea why php tries to open / read/write  deny / rw,
  # allow sending signals to our subprocesses  signal (send) peer=php-fpm//*,
  # allow switching processes to those subprofiles  change_profile -> php-fpm//*,
  # load all files from this directory  # store your configurations per pool in this dir  include if exists <php-fpm.d>
  # Site-specific additions and overrides. See local/README for details.  include if exists <local/php-fpm>}

In some cases one config would be enough, but for multiple apps use aa-autodep. It runs indefinitely across reboots and updates all of the profiles at once.

1. Run an initial aa-autodep to create an approximate profile for a program - this lets AppArmor consider the program for monitoring.
2. Activate learning or complain mode for all profiled programs by entering aa-complain /etc/apparmor.d/*
3. Run the application. Ensure that the running program gets to access each file representing its access needs. As a result, the execution might run for several days through multiple system reboots.
4. Analyze the log with aa-logprof.
5. Repeat Step 3 and Step 4 to generate an optimal Systemic profile. Subsequent iterations generate fewer messages and run faster.
6. Edit the profiles in /etc/apparmor.d/ as required.
7. Return to enforce mode using aa-enforce /etc/apparmor.d/* which eventually enforces the rules of the profiles.
8. Rescan all kernel profiles to ensure no conflict.
9. Modifying an existing AppArmor profile
10. Monitor the system for AppArmor denials using dmesg, /var/log/kern.log, /var/log/messages, aa-notify, auditd, journalctl11. Run aa-logprof to update the policy

Existing profiles:

https://gitlab.com/apparmor/apparmor/wikis/Profiles