CKSKubernetes 1.35

Lessons

AppArmor: write the profile, load it, attach it

updated 2026-10-22 by seed

What AppArmor gives you

A per-program mandatory access control profile: path-based rules for file access,
network, capabilities and mount. It is loaded on the node (the kernel does the
enforcing) and attached to a container through the Pod spec.

The task, end to end

"Block network access for the container in Pod web, using an AppArmor profile
named deny-network."

1. Write the profile on the node

/etc/apparmor.d/deny-network:

#include <tunables/global>

profile deny-network flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>

  network,
  deny /** w,
  deny /tmp/** rwklmx,
}

For "no network at all" the profile simply omits the network, rule and the
container cannot create sockets. A profile that allows everything and denies one
path is the usual exam variant:

profile deny-write {
  #include <tunables/global>
  #include <abstractions/base>
  file,
  deny /tmp/** w,
}

2. Load it, on the node that runs the Pod

sudo apparmor_parser -r /etc/apparmor.d/deny-network
sudo aa-status | grep deny-network
# or: cat /sys/kernel/security/apparmor/profiles | grep deny-network

AppArmor requires the profile to be loaded on every node where the Pod can
land. If the task pins the Pod with nodeName or a nodeSelector, load it there.

3. Attach it — modern field first

apiVersion: v1
kind: Pod
metadata: {name: web, namespace: prod}
spec:
  containers:
  - name: web
    image: nginx:1.27
    securityContext:
      appArmorProfile:
        type: Localhost
        localhostProfile: deny-network      # file name in /etc/apparmor.d

type is RuntimeDefault, Localhost or Unconfined. GA since 1.31, so at
v1.35 this is the field the marker reads.

3b. The legacy form, still accepted

metadata:
  annotations:
    container.apparmor.security.beta.kubernetes.io/web: localhost/deny-network

The annotation is per container name. It still works but is deprecated —
use it only if the task explicitly shows it.

4. Prove it

kubectl exec -n prod web -- cat /proc/self/attr/current
# deny-network (enforce)

kubectl exec -n prod web -- sh -c 'echo hi > /tmp/x'    # Permission denied

/proc/self/attr/current is the ground truth: it prints the profile actually
applied to that process. If it says docker-default (enforce) or unconfined,
your profile did not attach.

Traps

  • Attaching a profile that is not loaded → the container fails to start with apparmor: profile not found.
  • Attaching on the wrong node: the Pod runs elsewhere and the profile is absent.
  • localhostProfile is the profile name (file name in /etc/apparmor.d), not a path.
  • Editing /etc/apparmor.d/* without apparmor_parser does nothing — the kernel has the old copy.
  • aa-status shows whether AppArmor is enabled at all; on a kernel booted with apparmor=0 nothing you do will apply.