Mandatory Access Control

AppArmor

Path-based mandatory access control with per-program profiles.

AppArmor is the mandatory access control system you will actually run on Ubuntu. Its appeal is that the policy is legible: a profile is a plain-text list of the files a program may read or write, the capabilities it may use, and the network operations it may perform, all keyed by filesystem path. You can read an nginx profile and understand in a minute what nginx is and is not allowed to touch. That readability, plus a workflow for generating profiles from observed behaviour, makes AppArmor practical to adopt service by service, which is why it is the Ubuntu and SUSE default. The trade-off versus SELinux (SELinux page) is that path-based rules can be less precise than labels, but for most workloads the clarity wins.

Profile syntax at a glance

A profile lives in /etc/apparmor.d/, named after the binary it confines. It attaches to an executable path and lists permissions. A trimmed example for a small daemon:

#include <tunables/global>

/usr/local/bin/myapp {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  capability net_bind_service,

  /usr/local/bin/myapp        mr,      # read + execute the binary itself
  /etc/myapp/**               r,       # read its config tree
  /var/lib/myapp/**           rw,      # read/write its data
  /var/log/myapp/app.log      w,       # write its log
  network tcp,

  deny /etc/shadow            r,       # explicit denial, always wins
}

The permission letters after each path are the grammar: r read, w write, m memory-map executable, x execute (with modifiers), k lock, l link. Rules are allow-by-listing: anything not granted is implicitly denied. An explicit deny rule takes precedence over any allow and is how you carve exceptions out of a broad grant like /etc/**.

Enforce vs complain mode

AppArmor profiles run in one of two modes, and the difference is central to how you adopt and debug them.

ModeBehaviourWhen to use
enforceViolations are blocked and loggedProduction; the actual protection
complainViolations are allowed but loggedLearning what a program does before locking it down

The workflow is: put a new profile in complain mode, exercise the application through its real workload, collect the logged accesses, fold them into the profile, then switch to enforce. aa-complain and aa-enforce flip a single profile between modes; aa-status shows the current split (as seen on the Mandatory Access Control overview page).

# Put a profile into complain mode, run the app, then check status
sudo aa-complain /usr/local/bin/myapp
sudo aa-status | grep -A3 'complain mode'

Expected output:

4 profiles are in complain mode.
   /usr/local/bin/myapp
   /usr/sbin/dovecot
   ...

Generating a profile from behaviour

You rarely write a profile from a blank page. aa-genprof starts a profile and watches the program; aa-logprof reads the accumulated complain-mode logs and interactively proposes rules for each access it saw. This is the fastest path to a working profile.

# Learn a profile interactively while you exercise the app in another terminal
sudo aa-genprof /usr/local/bin/myapp

Expected output:

Profiling: /usr/local/bin/myapp
Please start the application to be profiled in another window and exercise its functionality now.
[(S)can system log for AppArmor events] / (F)inish

After you drive the app and press S, it walks you through each observed access:

Reading log entries from /var/log/syslog.
Profile:  /usr/local/bin/myapp
Path:     /var/lib/myapp/state.db
Mode:     rw
[(A)llow] / (D)eny / (I)gnore / (G)lob / ...

Choosing Allow (often with Glob to generalise a path to /var/lib/myapp/**) writes the rule. When you finish, the profile is saved and loaded. Always review the generated file by hand afterward: aa-genprof will happily grant whatever it observed, including accidental accesses you would not want to bless permanently.

Reading and fixing denials

When an enforced profile blocks something, AppArmor logs it to the kernel log and journal. That log entry is your fix-it ticket.

# See recent AppArmor denials
sudo dmesg | grep -i apparmor | tail -3

Expected output:

audit: type=1400 apparmor="DENIED" operation="open" profile="/usr/local/bin/myapp" name="/etc/myapp/extra.conf" pid=9042 comm="myapp" requested_mask="r" denied_mask="r"

The entry names everything you need: the profile, the operation (open), the exact path denied (/etc/myapp/extra.conf), and the mask (r). If that access is legitimate, feed the log to aa-logprof to add the rule, or edit the profile to add /etc/myapp/extra.conf r, and reload with apparmor_parser -r. If the access is not legitimate, the denial just caught a bug or an attack, and the profile did its job.

Where AppArmor ships and how it maps to containers

AppArmor is enabled by default on Ubuntu and SUSE. Ubuntu ships confinement profiles for many packaged services and increasingly for unprivileged user namespaces (see the Namespaces page). Container runtimes apply AppArmor too: Docker attaches a default docker-default profile to containers, and in the Kubernetes Security foundation you select a profile per container via the appArmorProfile field (or, on older clusters, the container.apparmor.security.beta.kubernetes.io annotation). The profile syntax and enforce/complain workflow are identical whether the target is a host daemon or a container’s main process.

Practical Guidance

  1. Adopt profiles in complain mode first. Exercise the real workload, harvest the logged accesses with aa-logprof, then switch to enforce so you do not break the app on day one.
  2. Always hand-review a generated profile. aa-genprof grants what it saw, including one-off or accidental accesses; tighten globs and remove anything unexpected.
  3. Prefer explicit deny rules for sensitive paths (like /etc/shadow) even inside a broad grant. Deny always wins and documents intent.
  4. Treat a DENIED log line as a decision point, not a nuisance. Either the access is legitimate (add a precise rule) or it is not (you just caught a problem). Never blanket-disable the profile.
  5. Reload with apparmor_parser -r after edits and confirm with aa-status. Do not unload a profile to “test something” and forget to reload it.