AppArmor and seccomp on Kubernetes

AppArmor and seccomp shrink what a container can do after it starts. Admission policies decide whether the pod is allowed. These two decide what syscalls and file paths it gets once it is running.

AppArmor is about files, network, and capabilities. Seccomp is about syscalls. Both are kernel features. Kubernetes only attaches a profile that already exists on the node.

AppArmor

Modes:

  • enforce — block violations
  • complain — log and allow (use this to learn the profile)
  • unconfined — no profile
sudo systemctl status apparmor
sudo aa-status

Profiles live in /etc/apparmor.d/. Load one, then enforce it:

sudo apparmor_parser -r /etc/apparmor.d/nginx-profile
sudo aa-enforce nginx-profile
sudo aa-complain nginx-profile   # while you are writing it

A tight nginx-shaped profile denies /etc/shadow, /root/, and sys_admin / net_admin, and allows the paths nginx actually needs.

Attach it with a per-container annotation. The name after localhost/ must match the profile name on every node that might run the pod:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-apparmor
  annotations:
    container.apparmor.security.beta.kubernetes.io/nginx: localhost/nginx-profile
spec:
  containers:
  - name: nginx
    image: nginx:1.20

If the profile is missing on a worker, the pod will not start. That is the usual failure.

Violations show up in the kernel log:

sudo dmesg | grep -i apparmor
kubectl describe pod nginx-apparmor

Seccomp

Modes you will actually use:

  • RuntimeDefault — the runtime’s default filter. Start here.
  • Localhost — a JSON file under the kubelet seccomp directory
  • Unconfined — no filter. Avoid it.
apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: nginx:1.20

Custom profile:

sudo mkdir -p /var/lib/kubelet/seccomp/profiles
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/nginx-seccomp.json

The JSON defaultAction is usually SCMP_ACT_ERRNO. Then you allow the syscalls the process needs. Deny mount, ptrace, chmod if the workload does not need them. Validate the file with jq before you point a pod at it.

Container-level seccompProfile overrides the pod-level setting.

grep CONFIG_SECCOMP /boot/config-$(uname -r)
sudo dmesg | grep -i seccomp
kubectl get pod nginx-pod -o yaml | grep -A 10 securityContext

Write the AppArmor profile in complain mode first. Use RuntimeDefault seccomp unless you have a reason not to. Put the files on every node, not just the control plane.