Challenge
DifficultyMedium
Learner activity No attempts yet

Load and Enforce a Custom AppArmor Profile on a Kubernetes Node

A raw AppArmor profile has been placed on the worker node node-01. Load it into the kernel, label the node, and schedule an nginx Deployment onto it with the profile enforced via securityContext — then capture the Pod's logs as proof the profile is actually blocking writes.

Scenario

The security team wants proof that a custom AppArmor profile can be loaded on a node and actually enforced on a workload — not just attached as an unused annotation. A raw profile file has already been placed on node-01, but nothing has loaded it into the kernel yet.

Important

AppArmor is an optional Linux kernel module. Before relying on it, always confirm it's actually enabled on the node:

cat /sys/module/apparmor/parameters/enabled

A Y means the kernel supports it. If this challenge's checks seem to fail no matter what you do, this is the first thing to verify.


Task

  1. On node-01, load the AppArmor profile located at /opt/apparmor-profiles/deny-write into the kernel so it runs in enforce mode.
  2. Label node-01 with security=apparmor.
  3. In a Namespace named sec-workloads, create a Deployment named hardened-web with:
    • image nginx:alpine
    • a container named web
    • the k8s-deny-write profile enforced on that container via securityContext
    • a nodeSelector that schedules the Pod onto the labeled node
  4. The container is expected to fail to start because of the profile. Capture the Pod's logs and save them to /home/laborant/pod.log (run this from cplane-01).

⚒ Test Cases


Hint 1 — Loading a Profile Into the Kernel

Having a profile file on disk doesn't mean the kernel knows about it. AppArmor profiles need to be parsed and loaded explicitly, and by default they load in enforce mode (unless the profile itself declares a complain flag, which this one doesn't).

Look for the tool that ships with the apparmor-utils / apparmor packages for compiling and loading a profile file into the kernel. Once loaded, every profile the kernel knows about — and its current mode — is listed in a file under /sys/kernel/security/apparmor/.

Documentation

Hint 2 — Labeling and Selecting the Node

This is standard kubectl node labeling — the same mechanism used for any other node-targeting scheduling decision. Once the label exists, the Deployment's Pod template needs a matching nodeSelector under spec.template.spec.

Documentation

Hint 3 — Attaching the Profile to a Container

Since Kubernetes v1.30+, AppArmor is configured directly in a container's securityContext — not via the older, deprecated container.apparmor.security.beta.kubernetes.io/<container> annotation.

The relevant shape looks like this:

securityContext:
  appArmorProfile:
    type: Localhost
    localhostProfile: <profile-name-as-loaded-in-the-kernel>

The profile name must match exactly what the profile line inside the loaded AppArmor file declares — check the profile file on node-01 for the correct value.

Documentation

Hint 4 — Capturing the Failure Evidence

Denying all writes to an nginx container is not a gentle restriction — nginx needs to write temp files and its PID file on startup, so expect the Pod to end up in CrashLoopBackOff or Error. That failure is the point: it's your evidence that the profile is actually being enforced, not just attached and ignored.

kubectl logs works the same way on a crashing container as on a healthy one. Redirect its output to the required file with your shell's redirection operator.

If the log file comes back empty, double check you're pointed at the right Pod and namespace.

Problem
Solution
Start