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:
| Objective | What it means in practice |
|---|---|
| Minimize host OS footprint | Remove packages, disable services, and close ports that aren’t needed to run kubelet and the container runtime |
| Minimize IAM/identity exposure | Restrict who and what can reach the node and its credentials (SSH, metadata, service accounts) |
| Minimize external access to the network | Use host firewalls and NetworkPolicy to limit reachable ports and east-west traffic |
| Use kernel hardening tools | Confine 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/podannotation. That annotation has been removed in modern Kubernetes — use theseccompProfilefield. 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
localhostProfilepath is relative to the kubelet’s seccomp root (/var/lib/kubelet/seccomp/), not an absolute path. SCMP_ACT_LOGlogs syscalls without blocking — useful to discover what an app actually needs before tightening toSCMP_ACT_ERRNO.- Common
defaultActionvalues: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 theappArmorProfilefield — 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
| Task | Where | Key command / field |
|---|---|---|
| Find listening ports | node | ss -tulpn |
| Disable a service | node | systemctl disable --now <svc> |
| Remove a package | node | apt-get purge / dnf remove |
| Blacklist a kernel module | node | /etc/modprobe.d/*.conf + modprobe -r |
| Host firewall default-deny | node | ufw default deny incoming |
| Runtime-default syscall filter | pod | seccompProfile.type: RuntimeDefault |
| Custom seccomp profile | node + pod | /var/lib/kubelet/seccomp/, type: Localhost |
| Load AppArmor profile | node | apparmor_parser -q <file> |
| Attach AppArmor profile | pod | appArmorProfile.type: Localhost |
| Verify AppArmor confinement | node | aa-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.