CKS — Mock Exam Part 1

13 minute read

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:

  1. Create a NetworkPolicy named default-deny-all in namespace secure-ns that denies all ingress AND egress traffic to/from all pods in the namespace.

  2. Create a second NetworkPolicy named allow-dns in namespace secure-ns that allows DNS egress (UDP and TCP port 53) to CoreDNS for all pods.

Verification:

  • Pods in secure-ns should be able to resolve DNS names
  • Pods in secure-ns should 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 Ingress and Egress in policyTypes creates 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:

  1. Run kube-bench against the API server (master node) checks only.
  2. From the results, fix the following three FAIL items:
    • Ensure that the --anonymous-auth argument is set to false
    • Ensure that the --profiling argument is set to false
    • Ensure that the --insecure-port argument is set to 0

Verification:

  • Re-run kube-bench and 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:

  1. Create a Role named dev-reader in namespace dev that allows:
    • get, list, watch on pods
    • get, list on configmaps
  2. Create a RoleBinding named dev-reader-binding in namespace dev that binds the Role dev-reader to ServiceAccount app-sa in namespace dev.

Verification:

  • ServiceAccount app-sa can get/list pods in dev
  • ServiceAccount app-sa can get/list configmaps in dev
  • ServiceAccount app-sa CANNOT create or delete pods in dev
  • ServiceAccount app-sa CANNOT 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 (not ClusterRole) because permissions are scoped to namespace dev
  • 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:

  1. Modify the default ServiceAccount in namespace production to disable automatic token mounting.
  2. 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: false on the ServiceAccount applies to all pods using that SA
  • A pod can override this by setting automountServiceAccountToken: true in 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:

  1. SSH to node01 and create an AppArmor profile named restrict-etc-write that:
    • Allows all file operations by default
    • Denies write access to everything under /etc/
  2. Load the profile on node01.

  3. Create a Pod named secure-app in namespace default with:
    • Image: nginx
    • Container name: web
    • AppArmor profile: restrict-etc-write applied to the web container
    • The Pod must be scheduled on node01

Verification:

  • The Pod is in Running state
  • 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 node
  • flags=(attach_disconnected) is required for container use
  • nodeName: node01 ensures 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:

  1. Create a Pod named seccomp-pod in namespace default with:
    • Image: nginx
    • Container name: app
    • Seccomp profile type: RuntimeDefault applied at the pod level
  2. Create a second Pod named seccomp-pod-strict in namespace default with:
    • Image: nginx
    • Container name: app
    • Seccomp profile type: RuntimeDefault applied at the container level

Verification:

  • Both pods are in Running state
  • 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
  • RuntimeDefault uses the container runtime’s built-in default profile (recommended for most workloads)
  • For the CKS exam, RuntimeDefault is the most commonly tested Seccomp type
  • The other types are Unconfined (no restrictions) and Localhost (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:

  1. SSH to node01 and modify the SSH configuration to:
    • Disable root login
    • Disable password authentication (only SSH keys allowed)
    • Disable empty passwords
  2. 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 access
  • PasswordAuthentication no — forces SSH key-based authentication only
  • PermitEmptyPasswords no — rejects login with empty passwords
  • You must restart sshd for 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:

  1. Determine the version of the installed kubectl binary.
  2. Download the official checksum for that version from the Kubernetes release page.
  3. Verify the integrity of the installed kubectl binary using sha512sum.
  4. 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
  • sha512sum and sha256sum are 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

Updated: