Learn AppArmor - Web & Application Services
Episode 9 of 23

Learn AppArmor - Web & Application Services

Applying AppArmor to real services: the built-in profiles for Nginx, Apache, and OpenSSH, customizing web roots and SSL certificates, MySQL and PostgreSQL database profiles, Node and Python runtimes, and the log rotation traps.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In episode 8, you gained the tools to write portable profiles: variables, tunables, and abstractions. Now it's time to step out of the practice sessions and lock down the services actually running on your servers. Web servers and databases are the first targets almost always attacked when an instance is internet-facing — and yet they're exactly the easiest to secure, because the community has already prepared built-in profiles.

Distribution Built-in Profiles

Ubuntu and Debian ship AppArmor profiles for popular services inside their packages. Some are active right after installation; others are available but waiting to be activated. See what's on your system:

List built-in service profiles
ls /etc/apparmor.d/ | grep -E "nginx|apache|sshd|mysqld|postgres"

Example filenames that appear:

Built-in profiles in the profile directory
usr.sbin.apache2
usr.sbin.mysqld
usr.sbin.nginx
usr.sbin.sshd
usr.lib.postgresql.14.bin.postgres

Notice the pattern: the filename is the executable path with / replaced by dots. When Nginx runs from /usr/sbin/nginx, the kernel loads the profile usr.sbin.nginx. This relationship determines the filename — not the service name.

Note

Profile status can be seen with aa-status: a (enforce) line means actively denying, (complain) means only logging, and (attach_disconnected) is a special mode often used by container profiles. A built-in profile not listed in aa-status isn't active — you need to activate it explicitly.

Customizing the Nginx Profile

A built-in profile is usually enough for a standard installation. Problems appear when you deviate from the defaults: the web root moves, certificates go to Let's Encrypt, or logs are redirected to a custom directory. A common customization example:

Customizing the usr.sbin.nginx profile
#include <tunables/global>
 
/usr/sbin/nginx flags=(enforce) {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/ssl_certs>
 
  network inet tcp,
  network inet6 tcp,
 
  /usr/sbin/nginx mr,
  /etc/nginx/** r,
  /var/www/app/** r,
  /var/log/nginx/*.log w,
 
  # Let's Encrypt certificates for custom domains
  /etc/letsencrypt/live/**.pem r,
  /etc/letsencrypt/archive/** r,
}

Three customizations you must pay attention to:

  • /var/www/app/** r — your web root isn't the default /var/www/html.
  • /etc/letsencrypt/** — certbot certificates live outside /etc/ssl.
  • /var/log/nginx/*.log w — the profile must be able to write the logs that logrotate newly opens.

Don't forget to reload after editing, following the pattern from episode 7:

Reload the nginx profile
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx

OpenSSH: Few Rules, Big Impact

usr.sbin.sshd is the simplest profile worth activating: SSH is a server's main entry door, and shrinking what the sshd process can do after being compromised is extremely valuable. The built-in profile allows authentication, session opening, and basic operations — while most other system functions stay closed. If you use key-based authentication with a custom key location, make sure a rule for @{HOME}/.ssh/** r is present in the profile.

Database Profiles: MySQL and PostgreSQL

Databases store your most valuable data, so their profiles must be genuinely locked down. usr.sbin.mysqld allows access to the data directory and local sockets:

Core mysqld profile rules
/var/lib/mysql/** rw,
/var/run/mysqld/** rw,
/var/log/mysql/*.log rw,

Pay special attention to log rotation. Logrotate closes the old log file and creates a new one. If the profile only allows a specific file path (for example /var/log/mysql/error.log), the freshly rotated file will trigger a denial. Use a wildcard for the whole directory, and make sure there's a rule for the directory itself:

Safe wildcard rules for log rotation
/var/log/mysql/ rw,
/var/log/mysql/** rw,

PostgreSQL has a similar pattern through the usr.lib.postgresql.*.bin.postgres profile. If you change the data directory or port, update the profile — a connect denial to a socket at a custom path is the symptom that appears most often.

Application Runtimes: Node and Python

Runtimes like Node and Python are their own challenge: they load modules dynamically, do JIT compilation, and open many small files at runtime. A profile that's too tight produces denial after denial; a profile that's too loose is useless.

A realistic approach for an application on a server:

A Node application profile
#include <tunables/global>
 
/usr/bin/node mr,
/opt/myapp/** r,
/opt/myapp/node_modules/** r,
/opt/myapp/.env r,
/var/log/myapp/*.log rw,
 
network inet tcp,
capability setgid,
capability setuid,

Notice what's included and what isn't. The project paths are watched fully, while Node's internals are handled by abstractions/base or explicit rules. For applications with native extensions, JIT may need the m (memory map) right for loaded libraries — an operation="mmap" denial in the logs is the clue.

Tip

The staged strategy works best here: run the runtime in complain mode (aa-complain /etc/apparmor.d/...), let the application work for a few days, collect the denials that genuinely repeat with aa-logprof, then move to enforce. Complain mode turns the logs into a list of needs without blocking anything.

Conclusion

In episode 9 you've applied AppArmor to production services: recognizing the distribution built-in profiles for Nginx, Apache, and OpenSSH, customizing web roots and SSL certificates at custom locations, understanding the core MySQL and PostgreSQL rules, handling the dynamics of Node and Python runtimes, and avoiding the log rotation traps with the right wildcards.

Keys to take home:

  • The profile filename comes from the executable path, not the service name.
  • Customize the profile when you deviate from the installation defaults, then reload.
  • Use directory wildcards for paths that change during rotation.

All the profiles we've discussed so far lock down processes running directly on the host. But most modern workloads no longer run directly on the host — they run in containers. In episode 10, we move into Containers & Kubernetes: AppArmor integration with Docker and containerd, the docker-default profile, and securing pods in Kubernetes with annotations and native fields.

Learn AppArmor - Web & Application Services | Learn AppArmor