CKS — Mock Exam Part 1
Mock Exam Instructions
This mock exam covers CKS Domains 1–3: Cluster Setup, Cluster Hardening, and System Hardening.
Rules
- Time limit: 60 minutes for all 8 questions
- Open book: Use only kubernetes.io/docs, kubernetes.io/blog, and github.com/kubernetes
- No peeking at answers until you have attempted every question or time runs out
- Score yourself honestly — partial credit for partially correct answers
Scoring
| Rating | Criteria |
|---|---|
| Full marks | Task completed correctly, resource verified working |
| Partial credit (50%) | Correct approach but minor error (typo, missing field) |
| No credit | Fundamentally wrong approach or not attempted |
Estimated Time per Question
| Question | Topic | Est. Time | Points |
|---|---|---|---|
| Q1 | NetworkPolicy: Default Deny + DNS | 5 min | 8 |
| Q2 | kube-bench: Fix API Server Failures | 8 min | 12 |
| Q3 | RBAC: Create Role + RoleBinding | 4 min | 8 |
| Q4 | ServiceAccount: Disable Token Auto-mount | 3 min | 6 |
| Q5 | AppArmor: Profile + Pod | 8 min | 14 |
| Q6 | Seccomp: RuntimeDefault Profile | 4 min | 8 |
| Q7 | SSH Hardening | 5 min | 8 |
| Q8 | Binary Verification | 3 min | 6 |
| Total | 40 min | 70 |
You have 60 minutes — the extra 20 minutes is your buffer for debugging and review.
Question 1: Default Deny NetworkPolicy + Allow DNS Egress
Context: kubectl config use-context cluster1-admin@cluster1
Scenario:
The namespace secure-ns exists and contains several application pods. Currently, there are no NetworkPolicies in this namespace — all pods can communicate freely with any destination inside or outside the cluster.
Task:
-
Create a NetworkPolicy named
default-deny-allin namespacesecure-nsthat denies all ingress AND egress traffic to/from all pods in the namespace. -
Create a second NetworkPolicy named
allow-dnsin namespacesecure-nsthat allows DNS egress (UDP and TCP port 53) to CoreDNS for all pods.
Verification:
- Pods in
secure-nsshould be able to resolve DNS names - Pods in
secure-nsshould NOT be able to reach any other destination (ingress or egress)
Answer: Question 1
Step 1: Create the default deny NetworkPolicy
# default-deny-all.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: secure-ns
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
kubectl apply -f default-deny-all.yaml
Step 2: Create the DNS allow NetworkPolicy
# allow-dns.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: secure-ns
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
kubectl apply -f allow-dns.yaml
Verification:
# DNS should work
kubectl exec -n secure-ns <pod-name> -- nslookup kubernetes
# Should resolve
# Egress to other services should be blocked
kubectl exec -n secure-ns <pod-name> -- wget -qO- --timeout=2 http://nginx.default.svc
# Should timeout
# Ingress should be blocked (from another namespace)
kubectl exec -n default <pod-name> -- wget -qO- --timeout=2 http://<secure-ns-pod-ip>
# Should timeout
Key points:
podSelector: {}selects ALL pods in the namespace- Both
IngressandEgressinpolicyTypescreates a full deny - The DNS policy allows egress ONLY to port 53 for CoreDNS
- CoreDNS is identified by the label
k8s-app: kube-dns
Question 2: Run kube-bench and Fix API Server Failures
Context: kubectl config use-context cluster1-admin@cluster1
Scenario:
Your cluster’s API server has not been hardened according to CIS Kubernetes Benchmark. You need to identify and fix security issues.
Task:
- Run
kube-benchagainst the API server (master node) checks only. - From the results, fix the following three FAIL items:
- Ensure that the
--anonymous-authargument is set tofalse - Ensure that the
--profilingargument is set tofalse - Ensure that the
--insecure-portargument is set to0
- Ensure that the
Verification:
- Re-run
kube-benchand confirm the three items now show PASS - The API server must be running and healthy after changes
Answer: Question 2
Step 1: Run kube-bench
# Run kube-bench for master node checks only
kube-bench run --targets=master
# Or if kube-bench is installed as a container:
kubectl run kube-bench --image=aquasec/kube-bench:latest \
--restart=Never --rm -it -- run --targets=master
Step 2: Back up the API server manifest
cp /etc/kubernetes/manifests/kube-apiserver.yaml \
/etc/kubernetes/manifests/kube-apiserver.yaml.bak
Step 3: Edit the API server manifest
vi /etc/kubernetes/manifests/kube-apiserver.yaml
Add or modify the following flags in the command section:
spec:
containers:
- command:
- kube-apiserver
# ... existing flags ...
- --anonymous-auth=false
- --profiling=false
- --insecure-port=0
Notes on each flag:
| Flag | Before | After | Impact |
|---|---|---|---|
--anonymous-auth |
Not set (defaults to true) |
false |
Disables anonymous API access |
--profiling |
Not set (defaults to true) |
false |
Disables profiling endpoint |
--insecure-port |
Not set or non-zero | 0 |
Disables insecure HTTP port |
Step 4: Wait for API server to restart
# Watch for the API server to come back
watch crictl ps | grep kube-apiserver
# Or
watch kubectl get pods -n kube-system
The kubelet will automatically detect the manifest change and restart the API server pod. This typically takes 30–60 seconds.
Step 5: Verify
# Re-run kube-bench
kube-bench run --targets=master | grep -A2 "anonymous-auth\|profiling\|insecure-port"
# All three should show [PASS]
Common mistake: If the API server does not come back, you likely introduced a YAML syntax error. Restore the backup:
cp /etc/kubernetes/manifests/kube-apiserver.yaml.bak \
/etc/kubernetes/manifests/kube-apiserver.yaml
Question 3: RBAC — Create Role and RoleBinding
Context: kubectl config use-context cluster1-admin@cluster1
Scenario:
The namespace dev exists with a ServiceAccount named app-sa. The development team requires read-only access to pods and configmaps in this namespace.
Task:
- Create a Role named
dev-readerin namespacedevthat allows:get,list,watchonpodsget,listonconfigmaps
- Create a RoleBinding named
dev-reader-bindingin namespacedevthat binds the Roledev-readerto ServiceAccountapp-sain namespacedev.
Verification:
- ServiceAccount
app-sacan get/list pods indev - ServiceAccount
app-sacan get/list configmaps indev - ServiceAccount
app-saCANNOT create or delete pods indev - ServiceAccount
app-saCANNOT list pods in other namespaces
Answer: Question 3
Step 1: Create the Role
kubectl create role dev-reader \
--verb=get,list,watch --resource=pods \
--verb=get,list --resource=configmaps \
-n dev
Or with YAML:
# dev-reader-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: dev-reader
namespace: dev
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
kubectl apply -f dev-reader-role.yaml
Step 2: Create the RoleBinding
kubectl create rolebinding dev-reader-binding \
--role=dev-reader \
--serviceaccount=dev:app-sa \
-n dev
Or with YAML:
# dev-reader-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dev-reader-binding
namespace: dev
subjects:
- kind: ServiceAccount
name: app-sa
namespace: dev
roleRef:
kind: Role
name: dev-reader
apiGroup: rbac.authorization.k8s.io
kubectl apply -f dev-reader-binding.yaml
Step 3: Verify
# Should return "yes"
kubectl auth can-i get pods \
--as=system:serviceaccount:dev:app-sa -n dev
kubectl auth can-i list configmaps \
--as=system:serviceaccount:dev:app-sa -n dev
# Should return "no"
kubectl auth can-i create pods \
--as=system:serviceaccount:dev:app-sa -n dev
kubectl auth can-i delete pods \
--as=system:serviceaccount:dev:app-sa -n dev
# Should return "no" (different namespace)
kubectl auth can-i get pods \
--as=system:serviceaccount:dev:app-sa -n default
Key points:
- Use
Role(notClusterRole) because permissions are scoped to namespacedev - Both the Role and RoleBinding must be in the same namespace
- ServiceAccount format in binding:
--serviceaccount=<namespace>:<name> - Using the imperative command is fastest; YAML is fallback
Question 4: Disable ServiceAccount Token Auto-Mounting
Context: kubectl config use-context cluster2-admin@cluster2
Scenario:
The namespace production exists. Pods in this namespace should not automatically receive a ServiceAccount token unless explicitly required.
Task:
- Modify the
defaultServiceAccount in namespaceproductionto disable automatic token mounting. - Verify that new pods created without explicitly requesting a token do NOT have a token mounted at
/var/run/secrets/kubernetes.io/serviceaccount/.
Answer: Question 4
Step 1: Edit the default ServiceAccount
kubectl edit serviceaccount default -n production
Add automountServiceAccountToken: false:
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: production
automountServiceAccountToken: false
Or use kubectl patch:
kubectl patch serviceaccount default -n production \
-p '{"automountServiceAccountToken": false}'
Step 2: Create a test pod
kubectl run test-no-token --image=nginx -n production
Step 3: Verify no token is mounted
# Check if the token volume is present
kubectl get pod test-no-token -n production -o yaml | grep -A5 "serviceAccount"
# Exec into the pod and check
kubectl exec test-no-token -n production -- ls /var/run/secrets/kubernetes.io/serviceaccount/
# Should return an error or empty (no token files)
# Alternative: check volumeMounts
kubectl get pod test-no-token -n production \
-o jsonpath='{.spec.containers[0].volumeMounts}' | python3 -m json.tool
# Should not contain a serviceaccount token mount
Step 4: Clean up test pod
kubectl delete pod test-no-token -n production --force --grace-period=0
Key points:
automountServiceAccountToken: falseon the ServiceAccount applies to all pods using that SA- A pod can override this by setting
automountServiceAccountToken: truein its own spec - This is a CIS benchmark recommendation for production namespaces
- Existing running pods are NOT affected — only new pods
Question 5: AppArmor — Create Profile and Apply to Pod
Context: kubectl config use-context cluster1-admin@cluster1
Scenario:
Worker node node01 runs applications that should not be allowed to write to the /etc/ directory.
Task:
- SSH to
node01and create an AppArmor profile namedrestrict-etc-writethat:- Allows all file operations by default
- Denies write access to everything under
/etc/
-
Load the profile on
node01. - Create a Pod named
secure-appin namespacedefaultwith:- Image:
nginx - Container name:
web - AppArmor profile:
restrict-etc-writeapplied to thewebcontainer - The Pod must be scheduled on
node01
- Image:
Verification:
- The Pod is in
Runningstate - Writing to
/etc/from inside the container fails - Writing to other directories (e.g.,
/tmp/) succeeds
Answer: Question 5
Step 1: SSH to node01 and create the AppArmor profile
ssh node01
cat << 'EOF' > /etc/apparmor.d/restrict-etc-write
#include <tunables/global>
profile restrict-etc-write flags=(attach_disconnected) {
#include <abstractions/base>
# Allow all file operations by default
file,
# Deny write access to /etc/ and all subdirectories
deny /etc/** w,
deny /etc/ w,
}
EOF
Step 2: Load the profile
apparmor_parser -r /etc/apparmor.d/restrict-etc-write
Step 3: Verify the profile is loaded
aa-status | grep restrict-etc-write
# Should show "restrict-etc-write" in the loaded profiles list
Step 4: Exit the node
exit
Step 5: Create the Pod with AppArmor annotation
# secure-app.yaml
apiVersion: v1
kind: Pod
metadata:
name: secure-app
namespace: default
annotations:
container.apparmor.security.beta.kubernetes.io/web: localhost/restrict-etc-write
spec:
nodeName: node01
containers:
- name: web
image: nginx
kubectl apply -f secure-app.yaml
Step 6: Verify
# Check Pod is Running
kubectl get pod secure-app
# STATUS should be Running
# Test that writing to /etc/ is denied
kubectl exec secure-app -- touch /etc/test-file
# Should fail with "Permission denied"
# Test that writing to /tmp/ still works
kubectl exec secure-app -- touch /tmp/test-file
# Should succeed
# Verify AppArmor annotation is applied
kubectl get pod secure-app -o yaml | grep apparmor
# Should show the annotation
Key points:
- The AppArmor profile MUST be loaded on the node before the Pod is created
- The annotation key must include
container.prefix:container.apparmor.security.beta.kubernetes.io/<container-name> <container-name>must match.spec.containers[].name(in this case,web)localhost/<profile-name>references a profile loaded on the local nodeflags=(attach_disconnected)is required for container usenodeName: node01ensures the pod runs on the node where the profile is loaded
Question 6: Seccomp — Apply RuntimeDefault Profile
Context: kubectl config use-context cluster1-admin@cluster1
Scenario:
All pods in the cluster should use the container runtime’s default Seccomp profile to restrict dangerous system calls.
Task:
- Create a Pod named
seccomp-podin namespacedefaultwith:- Image:
nginx - Container name:
app - Seccomp profile type:
RuntimeDefaultapplied at the pod level
- Image:
- Create a second Pod named
seccomp-pod-strictin namespacedefaultwith:- Image:
nginx - Container name:
app - Seccomp profile type:
RuntimeDefaultapplied at the container level
- Image:
Verification:
- Both pods are in
Runningstate - Both pods have the Seccomp profile visible in their spec
Answer: Question 6
Step 1: Create the pod-level Seccomp pod
# seccomp-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: seccomp-pod
namespace: default
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: nginx
kubectl apply -f seccomp-pod.yaml
Step 2: Create the container-level Seccomp pod
# seccomp-pod-strict.yaml
apiVersion: v1
kind: Pod
metadata:
name: seccomp-pod-strict
namespace: default
spec:
containers:
- name: app
image: nginx
securityContext:
seccompProfile:
type: RuntimeDefault
kubectl apply -f seccomp-pod-strict.yaml
Step 3: Verify
# Check both pods are Running
kubectl get pods seccomp-pod seccomp-pod-strict
# Verify Seccomp profile on pod-level
kubectl get pod seccomp-pod -o jsonpath='{.spec.securityContext.seccompProfile}'
# Output: {"type":"RuntimeDefault"}
# Verify Seccomp profile on container-level
kubectl get pod seccomp-pod-strict \
-o jsonpath='{.spec.containers[0].securityContext.seccompProfile}'
# Output: {"type":"RuntimeDefault"}
Key points:
- Pod-level seccomp:
.spec.securityContext.seccompProfile— applies to ALL containers - Container-level seccomp:
.spec.containers[].securityContext.seccompProfile— applies to that container only - Container-level overrides pod-level if both are set
RuntimeDefaultuses the container runtime’s built-in default profile (recommended for most workloads)- For the CKS exam,
RuntimeDefaultis the most commonly tested Seccomp type - The other types are
Unconfined(no restrictions) andLocalhost(custom profile from/var/lib/kubelet/seccomp/)
Question 7: SSH Hardening
Context: kubectl config use-context cluster1-admin@cluster1
Scenario:
Worker node node01 has an insecure SSH configuration. You need to harden it.
Task:
- SSH to
node01and modify the SSH configuration to:- Disable root login
- Disable password authentication (only SSH keys allowed)
- Disable empty passwords
- Apply the changes and verify.
Answer: Question 7
Step 1: SSH to node01
ssh node01
Step 2: Back up the SSH config
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
Step 3: Edit the SSH configuration
vi /etc/ssh/sshd_config
Find and modify (or add) these lines:
PermitRootLogin no
PasswordAuthentication no
PermitEmptyPasswords no
Alternatively, use sed:
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?PermitEmptyPasswords.*/PermitEmptyPasswords no/' /etc/ssh/sshd_config
Step 4: Restart SSH service
systemctl restart sshd
Step 5: Verify
# Check the active configuration
sshd -T | grep -i "permitrootlogin\|passwordauthentication\|permitemptypasswords"
# Expected output:
# permitrootlogin no
# passwordauthentication no
# permitemptypasswords no
# Verify sshd is running
systemctl status sshd
Step 6: Exit the node
exit
Key points:
- Always back up the config before editing
PermitRootLogin no— prevents direct root SSH accessPasswordAuthentication no— forces SSH key-based authentication onlyPermitEmptyPasswords no— rejects login with empty passwords- You must restart
sshdfor changes to take effect - On the CKS exam, you may also be asked about additional hardening: disabling X11 forwarding, setting
MaxAuthTries, etc.
Question 8: Verify kubectl Binary Integrity
Context: kubectl config use-context cluster1-admin@cluster1
Scenario:
There is a concern that the kubectl binary on the control plane node may have been tampered with.
Task:
- Determine the version of the installed kubectl binary.
- Download the official checksum for that version from the Kubernetes release page.
- Verify the integrity of the installed kubectl binary using
sha512sum. - Document whether the binary is authentic or tampered.
Answer: Question 8
Step 1: Determine the kubectl version
kubectl version --client
# Note the version, e.g., v1.29.0
Step 2: Find the kubectl binary location
which kubectl
# Typically: /usr/local/bin/kubectl or /usr/bin/kubectl
Step 3: Download the official checksum
# For sha256 (most commonly provided):
curl -LO "https://dl.k8s.io/v1.29.0/bin/linux/amd64/kubectl.sha256"
# For sha512:
curl -LO "https://dl.k8s.io/v1.29.0/bin/linux/amd64/kubectl.sha512"
Replace v1.29.0 with the actual version from Step 1.
Step 4: Verify the binary
# Using sha256:
echo "$(cat kubectl.sha256) $(which kubectl)" | sha256sum --check
# Expected output: /usr/local/bin/kubectl: OK
# Using sha512:
echo "$(cat kubectl.sha512) $(which kubectl)" | sha512sum --check
# Expected output: /usr/local/bin/kubectl: OK
Step 5: Interpret the result
- OK — The binary matches the official checksum. It is authentic.
- FAILED — The binary does NOT match. It may have been tampered with.
If tampered, replace the binary:
curl -LO "https://dl.k8s.io/v1.29.0/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl $(which kubectl)
Key points:
- Always check the version first — using the wrong version’s checksum will give a false FAILED result
- On the CKS exam, the question typically provides the expected hash or tells you where to download it
sha512sumandsha256sumare the standard tools for this verification- This is a quick 2–3 minute question worth easy points — do not skip it
Scoring Guide
After completing all 8 questions, score yourself:
| Question | Max Points | Your Score |
|---|---|---|
| Q1: NetworkPolicy Default Deny + DNS | 8 | ___ |
| Q2: kube-bench Fix | 12 | ___ |
| Q3: RBAC Role + Binding | 8 | ___ |
| Q4: SA Token Disable | 6 | ___ |
| Q5: AppArmor Profile + Pod | 14 | ___ |
| Q6: Seccomp RuntimeDefault | 8 | ___ |
| Q7: SSH Hardening | 8 | ___ |
| Q8: Binary Verification | 6 | ___ |
| Total | 70 | ___ |
How to Interpret Your Score
| Score | Readiness |
|---|---|
| 60–70 | Excellent — Domains 1–3 are solid |
| 45–59 | Good — Review the questions you missed |
| 30–44 | Needs work — Revisit the relevant chapters |
| Below 30 | Not ready — Go through Domains 1–3 tutorial material again |