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: