AppArmor: confine behavior

Load a profile, reference it, enforce vs complain.

Advanced14 min · lesson 12 of 24

seccomp controls which syscalls a container may make, but it says nothing about which files that container may read or write — a process can be allowed the open syscall and still open anything on the filesystem. AppArmor fills that gap. It is a Linux Security Module that confines a program to a declared set of file paths, Linux capabilities, and network operations, so a compromised app that could previously write anywhere is reduced to writing the three paths its profile permits.

AppArmor is path-based and loaded per node. Confirm the kernel module is enabled, then load profiles with apparmor_parser and inspect state with aa-status, which lists every loaded profile and whether it is in enforce or complain mode. Because it is a host feature, the profile must exist on whatever node the pod lands on.

terminal
$ cat /sys/module/apparmor/parameters/enabled # -> Y (module active)
$ sudo apparmor_parser -q /etc/apparmor.d/k8s-frontend # load a profile
$ sudo aa-status
apparmor module is loaded.
12 profiles are loaded. 12 in enforce mode, 0 in complain mode.
k8s-frontend
docker-default ...

What a profile looks like

A profile is a set of allow rules over paths and capabilities; anything not permitted is denied. A tight web-server profile might allow reading its static content and binding a socket, deny writes to the binary directories, and drop capabilities it never uses. The shape below confines a frontend to read-only content and an explicit writable temp path.

/etc/apparmor.d/k8s-frontend
#include <tunables/global>
profile k8s-frontend flags=(attach_disconnected) {
#include <abstractions/base>
network inet tcp, # allow TCP
/usr/share/nginx/** r, # read static content
/var/cache/nginx/** rw, # explicit writable path
deny /bin/** wl, # no writing into binary dirs
deny /etc/shadow rwkl, # never touch the shadow file
}

Reference it from the pod

Recent Kubernetes sets the profile as a field on the security context (type Localhost, plus the profile name); older clusters used a container.apparmor.security.beta.kubernetes.io/<container> annotation — you will still see the annotation in the wild and in some exam environments. Either way the named profile must already be loaded on the node, so load it fleet-wide with a DaemonSet or your node bootstrap.

pod-apparmor.yaml
spec:
containers:
- name: app
image: registry.internal/frontend:1.4.2
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: k8s-frontend
# legacy form (still common):
# metadata.annotations:
# container.apparmor.security.beta.kubernetes.io/app: localhost/k8s-frontend
Complain before enforce; a missing profile fails the pod
Load a new profile in complain mode (aa-complain) so violations are logged, not blocked — exercise the app, read the denials in the audit log, tighten, then aa-enforce. And note: if a pod references a profile that is not loaded on its node, the pod fails to start. Load first, reference second.