AppArmor profiles are surprisingly not about preventing privilege escalation, but about enforcing least privilege for already running processes.

Imagine you have a web server, like Nginx, running on your system. You want to ensure it can only read its configuration files, write logs, and serve static content from a specific directory. You don’t want it to be able to execute arbitrary commands, access user home directories, or write to system binaries. AppArmor lets you define exactly these boundaries.

Here’s a typical AppArmor profile for Nginx, often found in /etc/apparmor.d/usr.sbin.nginx:

#include <tunables/global>

/usr/sbin/nginx {
  #include <abstractions/base>

  /etc/nginx/nginx.conf r,
  /etc/nginx/conf.d/ r,
  /etc/nginx/sites-available/ r,
  /etc/nginx/sites-enabled/ r,

  /var/log/nginx/access.log w,
  /var/log/nginx/error.log w,

  /var/www/html/ r,
  /var/www/html/** r,

  # Allow Nginx to bind to privileged ports if needed (e.g., 80, 443)
  capability net_bind_service,

  # Deny access to sensitive system files
  deny /etc/shadow r,
  deny /etc/passwd r,
  deny /etc/ssh/sshd_config r,
  deny /usr/bin/** ix, # Prevent execution of binaries
}

Let’s break down what’s happening here.

  • #include <tunables/global> and #include <abstractions/base>: These pull in pre-defined sets of rules. abstractions/base typically grants permissions needed for most common applications to run (like opening standard file descriptors). tunables allows for variables, making profiles more flexible.
  • /usr/sbin/nginx { ... }: This block defines the rules specifically for the Nginx executable.
  • /etc/nginx/nginx.conf r,: This line grants Nginx read (r) access to its main configuration file.
  • /etc/nginx/conf.d/ r, and /etc/nginx/sites-available/ r,, /etc/nginx/sites-enabled/ r,: These grant read access to directories where Nginx looks for additional configuration.
  • /var/log/nginx/access.log w, and /var/log/nginx/error.log w,: These allow Nginx to write (w) to its access and error log files.
  • /var/www/html/ r, and /var/www/html/** r,: This grants read access to the web root directory and all its contents.
  • capability net_bind_service,: This grants a specific Linux capability. net_bind_service allows a process to bind to privileged network ports (ports below 1024), which is necessary for Nginx to listen on HTTP (80) and HTTPS (443).
  • deny /etc/shadow r,, deny /etc/passwd r,, deny /etc/ssh/sshd_config r,: These are explicit denial rules. Even if some included abstraction allowed read access to these sensitive files, AppArmor’s deny rules take precedence.
  • deny /usr/bin/** ix,: This is a crucial rule. It denies Nginx the ability to execute (x) any file within the /usr/bin/ directory. This prevents Nginx from running arbitrary commands.

To activate these profiles, you first load them into the AppArmor kernel module:

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

The -r flag reloads the profile if it’s already loaded. After loading, you’ll typically restart the application (Nginx in this case) to ensure it starts under the new confinement:

sudo systemctl restart nginx

AppArmor operates in two modes: enforce and complain.

  • enforce mode: This is the default and most secure mode. If a process violates its profile, AppArmor blocks the action and logs the event.
  • complain mode: In this mode, AppArmor logs violations but does not block them. This is incredibly useful for developing and debugging profiles. You can run your application in complain mode, trigger all its functionalities, and then review the logs to identify what permissions are missing before switching to enforce mode.

You can check the status of AppArmor and your profiles with:

sudo aa-status

This command will show you which profiles are loaded, their mode (enforce/complain), and which applications are running under them.

When developing a profile, you’ll often find yourself using aa-genprof or aa-logprof.

  • sudo aa-genprof /usr/sbin/nginx: This utility helps you generate a profile by observing the application’s behavior. It runs the application, prompts you about each file access or capability usage, and builds a profile based on your answers.
  • sudo aa-logprof: This is the counterpart to aa-genprof but works on existing logs. It analyzes the system logs for AppArmor denial messages and helps you update your profiles to allow the necessary actions.

The real power of AppArmor isn’t in the syntax of the profiles themselves, but in how you can meticulously craft them to match the exact needs of an application. Instead of broad permissions, you’re providing surgical precision. This means that even if an attacker finds a vulnerability in Nginx (or any other confined application), the damage they can do is severely limited by the AppArmor profile. They can’t suddenly access /etc/shadow or execute commands from /bin/bash if the profile forbids it.

One of the less intuitive aspects of AppArmor profiles is how they handle network access. While you might explicitly allow bind operations for privileged ports, the profile doesn’t inherently grant access to all network sockets. You’ll often see rules like network inet stream, or specific network rules for UDP/TCP, to control the types of network communication allowed. Without these, even if the application can bind to a port, it might not be able to send or receive data over that socket if the profile is too restrictive.

The next step after mastering static profile creation is understanding how to integrate AppArmor with systemd services for dynamic profile management.

Want structured learning?

Take the full Cdk course →