Load and Enforce a Custom AppArmor Profile on a Kubernetes Node
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.
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
- On
node-01, load the AppArmor profile located at/opt/apparmor-profiles/deny-writeinto the kernel so it runs in enforce mode. - Label
node-01withsecurity=apparmor. - In a Namespace named
sec-workloads, create a Deployment namedhardened-webwith:- image
nginx:alpine - a container named
web - the
k8s-deny-writeprofile enforced on that container viasecurityContext - a
nodeSelectorthat schedules the Pod onto the labeled node
- image
- 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 fromcplane-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.