Back to Blog

System Hardening for the CKS Exam: AppArmor, seccomp, Kernel Hardening & Reducing Attack Surface

A hands-on guide to the System Hardening domain of the CKS exam — shrinking the host OS attack surface, restricting kernel modules, and confining containers with seccomp and AppArmor using securityContext fields, with copy-paste commands and YAML you can run on a real cluster.

By Sailor Team , June 25, 2026

The System Hardening domain of the CKS is the one candidates most often underestimate. It sits below the Kubernetes API entirely — it’s about the Linux host your nodes run on and the kernel-level confinement applied to individual containers. While Cluster Setup and Cluster Hardening lock down the control plane, System Hardening assumes an attacker has already landed inside a pod and asks: how much damage can they do to the node, and how do we shrink that blast radius?

This guide walks through the System Hardening objectives the way the exam tests them — hands-on, on a real node — covering host attack-surface reduction, kernel module restriction, and the two kernel-confinement tools you must know cold: seccomp and AppArmor. Every section has commands or manifests you can run today. If you want the full blueprint and exam logistics first, start with the CKS exam guide for 2026 and the CKS exam topics breakdown.

What System Hardening Covers

The CNCF curriculum lists four objectives under System Hardening:

ObjectiveWhat it means in practice
Minimize host OS footprintRemove packages, disable services, and close ports that aren’t needed to run kubelet and the container runtime
Minimize IAM/identity exposureRestrict who and what can reach the node and its credentials (SSH, metadata, service accounts)
Minimize external access to the networkUse host firewalls and NetworkPolicy to limit reachable ports and east-west traffic
Use kernel hardening toolsConfine containers with seccomp and AppArmor so a compromised process can’t make arbitrary syscalls or touch arbitrary files

The first three are about the node. The last is about the workload. On the exam you’ll see tasks from both halves, and the kernel-hardening tasks are the highest-value because they’re the most specific and most automatable.

Half One: Shrink the Host Attack Surface

A Kubernetes node should run as little as possible beyond the kubelet, the container runtime, and kube-proxy. Everything else is potential attack surface. The exam may give you a node with extra services running and ask you to disable them.

Audit and disable unnecessary services

# What's listening, and which process owns each port?
ss -tulpn

# What services are enabled at boot?
systemctl list-unit-files --type=service --state=enabled

# Disable and stop a service you don't need (example: a leftover web server)
systemctl disable --now apache2

systemctl disable --now both stops the running unit and prevents it starting at the next boot. Always verify with systemctl status <service> afterward.

Remove unneeded packages

# Debian/Ubuntu
apt list --installed 2>/dev/null | grep -i <tool>
apt-get purge -y <package>

# RHEL/CentOS
rpm -qa | grep -i <tool>
dnf remove -y <package>

Fewer packages means fewer CVEs and fewer binaries an attacker can pivot through. purge (not just remove) also deletes config files.

Close ports with the host firewall

# See current rules
iptables -L -n
# Or with ufw on Ubuntu
ufw status numbered

# Allow only what the node needs, then deny the rest
ufw allow 6443/tcp     # kube-apiserver (control plane node)
ufw allow 10250/tcp    # kubelet API
ufw default deny incoming
ufw enable

Exam tip: Don’t lock yourself out. Before flipping a default-deny firewall on a node you’re SSH’d into, make sure your SSH port (22) is explicitly allowed. The same caution applies to NetworkPolicy at the pod layer — pair host firewalling with a default-deny NetworkPolicy as described in the cluster hardening guide.

Restrict kernel modules

Loadable kernel modules expand the kernel’s attack surface. If a module isn’t required, blacklist it so it can’t be loaded — even by a privileged container.

# List currently loaded modules
lsmod

# Unload a module right now
modprobe -r dccp

# Prevent it from ever loading again
cat <<EOF > /etc/modprobe.d/blacklist-dccp.conf
blacklist dccp
install dccp /bin/true
EOF

The install <module> /bin/true line is the strong form — it makes any attempt to load the module run /bin/true instead, which is harder to bypass than blacklist alone.

Half Two: Confine Containers with seccomp

seccomp (secure computing mode) filters the system calls a process is allowed to make. A typical application needs only a few dozen of the ~300+ Linux syscalls; seccomp blocks the rest, so an exploit can’t call mount, ptrace, reboot, or other dangerous primitives.

Enable the runtime default profile

The single most impactful change is turning on the container runtime’s default profile, which blocks roughly 40+ dangerous syscalls while allowing everything a normal app needs. It is not on by default for pods — you must opt in via securityContext:

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: nginx:1.27

Setting it at the pod level applies it to every container. You can also set it per-container under containers[].securityContext.

Note on the old annotation: Older clusters used the seccomp.security.alpha.kubernetes.io/pod annotation. That annotation has been removed in modern Kubernetes — use the seccompProfile field. If a CKS task hands you a manifest still using the annotation, migrating it to the field is a common fix.

Use a custom (Localhost) profile

For tighter control, write a JSON profile, drop it on the node under /var/lib/kubelet/seccomp/, and reference it with type: Localhost:

// /var/lib/kubelet/seccomp/profiles/audit.json
{
  "defaultAction": "SCMP_ACT_LOG"
}
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/audit.json

Key facts for the exam:

  • The localhostProfile path is relative to the kubelet’s seccomp root (/var/lib/kubelet/seccomp/), not an absolute path.
  • SCMP_ACT_LOG logs syscalls without blocking — useful to discover what an app actually needs before tightening to SCMP_ACT_ERRNO.
  • Common defaultAction values: SCMP_ACT_ERRNO (block + return error), SCMP_ACT_ALLOW, SCMP_ACT_LOG.

Half Two, Continued: Confine the Filesystem with AppArmor

Where seccomp filters syscalls, AppArmor restricts what files, capabilities, and network operations a process can access using per-profile policy. On the CKS you’ll typically be given a profile and asked to load it and attach it to a pod.

Load the profile on the node

AppArmor profiles live on each node and must be loaded into the kernel before a pod can reference them:

# Load (or reload) a profile
apparmor_parser -q /etc/apparmor.d/k8s-deny-write

# Confirm it's loaded and in enforce mode
aa-status | grep k8s-deny-write

A minimal “deny all file writes” profile looks like this:

#include <tunables/global>

profile k8s-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  file,                 # allow read by default
  deny /** w,           # deny all writes
}

Attach the profile to a container

On Kubernetes 1.30+ AppArmor is GA and configured with the appArmorProfile field in securityContext:

apiVersion: v1
kind: Pod
metadata:
  name: hello-apparmor
spec:
  containers:
  - name: hello
    image: busybox:1.36
    command: ["sh", "-c", "echo 'try to write' > /tmp/x; sleep 1h"]
    securityContext:
      appArmorProfile:
        type: Localhost
        localhostProfile: k8s-deny-write

The profile type can be RuntimeDefault, Localhost (with localhostProfile), or Unconfined.

Version note: Clusters older than 1.30 used the annotation container.apparmor.security.beta.kubernetes.io/<container-name>: localhost/<profile> on the pod metadata. The exam targets a recent Kubernetes version, so lead with the appArmorProfile field — but recognize the annotation form, because you may need to read or convert it.

Verify enforcement

# Exec into the pod and prove the write is blocked
kubectl exec hello-apparmor -- sh -c 'echo test > /tmp/x'
# Expected: "Permission denied" if the profile is enforcing

# Check the profile a running process is confined by (on the node)
cat /proc/<pid>/attr/current

If the pod fails to start with a message like “cannot enforce AppArmor profile … profile not loaded”, the profile wasn’t loaded on the node where the pod was scheduled — go back and run apparmor_parser there.

Putting It Together: A Hardened Pod

A workload that satisfies the System Hardening expectations combines several controls at once. This is the pattern worth memorizing:

apiVersion: v1
kind: Pod
metadata:
  name: hardened
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: nginx:1.27
    securityContext:
      appArmorProfile:
        type: RuntimeDefault
      runAsNonRoot: true
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]

This single manifest delivers: runtime-default syscall filtering, AppArmor confinement, non-root execution, no privilege escalation, an immutable root filesystem, and all Linux capabilities dropped. The same securityContext discipline carries over into the Minimize Microservice Vulnerabilities domain, so practicing it pays off across the exam.

System Hardening Cheat Sheet

TaskWhereKey command / field
Find listening portsnodess -tulpn
Disable a servicenodesystemctl disable --now <svc>
Remove a packagenodeapt-get purge / dnf remove
Blacklist a kernel modulenode/etc/modprobe.d/*.conf + modprobe -r
Host firewall default-denynodeufw default deny incoming
Runtime-default syscall filterpodseccompProfile.type: RuntimeDefault
Custom seccomp profilenode + pod/var/lib/kubelet/seccomp/, type: Localhost
Load AppArmor profilenodeapparmor_parser -q <file>
Attach AppArmor profilepodappArmorProfile.type: Localhost
Verify AppArmor confinementnodeaa-status, cat /proc/<pid>/attr/current

Practice on a Real Cluster Before Exam Day

System Hardening is pure muscle memory. Knowing that seccomp’s runtime profile blocks dangerous syscalls is easy; reliably writing the securityContext block, dropping an AppArmor profile in /var/lib/kubelet/seccomp on the right node, loading it with apparmor_parser, and proving enforcement under a ticking clock is a different skill entirely — and it only comes from repetition on a live cluster.

Sailor.sh’s Certified Kubernetes Security Specialist (CKS) Mock Exam Bundle runs on a real Kubernetes cluster with exam-style performance tasks that mirror the format and difficulty of the actual CKS, including host-hardening and kernel-confinement scenarios like the ones above. If you’d rather warm up on the free, open-source practice terminal first, the CKS practice environment guide and how to practice CKS for free walk through getting started. Pair the hands-on work with a structured CKS study plan, confirm you’ve met the CKS prerequisites, and extend into the adjacent domains with the CKS supply chain security guide and the CKS runtime security and Falco guide.

Frequently Asked Questions

What is the difference between seccomp and AppArmor?

seccomp filters which system calls a process can make (blocking primitives like mount, ptrace, or reboot), while AppArmor restricts which files, capabilities, and network operations a process can use via a per-profile policy. They are complementary: seccomp narrows the syscall interface, AppArmor narrows resource access. The CKS expects you to apply both through securityContext.

How do I enable the default seccomp profile in Kubernetes?

Add a seccompProfile with type: RuntimeDefault to the pod or container securityContext. It is not enabled by default, so opting in is one of the highest-leverage hardening changes you can make. For tighter control, use type: Localhost with a custom JSON profile stored under /var/lib/kubelet/seccomp/.

Where do custom seccomp profiles have to be stored?

On each node under the kubelet’s seccomp root, /var/lib/kubelet/seccomp/. The localhostProfile value in the manifest is a path relative to that directory — for example profiles/audit.json resolves to /var/lib/kubelet/seccomp/profiles/audit.json. The profile must exist on whichever node the pod is scheduled to.

How do I apply an AppArmor profile to a pod on the CKS?

First load the profile on the node with apparmor_parser -q /etc/apparmor.d/<profile> and confirm with aa-status. Then reference it in the container’s securityContext.appArmorProfile using type: Localhost and localhostProfile: <profile>. On clusters older than 1.30 you’d use the container.apparmor.security.beta.kubernetes.io/<container> annotation instead.

Why won’t my pod start with an AppArmor profile?

The most common cause is that the profile isn’t loaded on the node where the pod landed. AppArmor profiles are node-local, so loading one on the control plane doesn’t help a pod scheduled to a worker. Load the profile on the correct node (or all nodes) with apparmor_parser, then verify with aa-status before re-creating the pod.

How much of the CKS is the System Hardening domain?

System Hardening is 10% of the CKS. It’s a smaller slice than the runtime, supply-chain, and microservice domains, but it’s high-yield because the tasks are concrete and quick once you’ve practiced them. See the CKS exam topics breakdown for the full weighting across all six domains.

Conclusion

System Hardening is where Kubernetes security meets the Linux host. The node should run nothing it doesn’t need, expose no port it doesn’t use, and load no kernel module it can’t justify. Every workload should run with the runtime-default seccomp filter, an AppArmor profile, dropped capabilities, and a read-only root filesystem — so that even a compromised process is boxed into the smallest possible set of actions.

These controls are quick to apply once they’re automatic, and the only way to make them automatic is to do them repeatedly on a real cluster. Work through the CKS mock exams, harden a node by hand, confine a pod with both seccomp and AppArmor, and prove each control actually blocks what it should. Do that, and the System Hardening domain becomes free points. For the full study path, begin with the CKS exam guide for 2026.

Limited Time Offer: Get 80% off all Mock Exam Bundles | Sale ends in 7 days. Start learning today.

Claim Now