CKS — Common Mistakes

14 minute read

Overview

These are the top 15 mistakes that fail CKS candidates. Every single one of these has cost real candidates real points on exam day. Study them, internalize them, and build habits that prevent them.

For each mistake, you will see:

  • What goes wrong — the actual failure scenario
  • How to avoid it — the habit or check that prevents it
  • Quick fix — what to do if you catch it during the exam

Mistake 1: Forgetting to Load AppArmor Profile on the Node

What Goes Wrong

You create a Pod with an AppArmor annotation like:

metadata:
  annotations:
    container.apparmor.security.beta.kubernetes.io/secure: localhost/my-profile

The Pod stays in Blocked status or fails to schedule. The AppArmor profile my-profile was never loaded on the target node.

How to Avoid It

Always load the profile on the node first, then create the Pod:

# 1. SSH to the node
ssh node01

# 2. Create the profile file
cat << 'EOF' > /etc/apparmor.d/my-profile
#include <tunables/global>

profile my-profile flags=(attach_disconnected) {
  #include <abstractions/base>
  file,
  deny /etc/** w,
}
EOF

# 3. Load the profile
apparmor_parser -r /etc/apparmor.d/my-profile

# 4. Verify it is loaded
aa-status | grep my-profile

# 5. Exit back to control plane
exit

# 6. NOW create the Pod
kubectl apply -f pod-with-apparmor.yaml

Quick Fix

If the Pod is stuck, SSH to the node, load the profile, delete the Pod, and recreate it.


Mistake 2: Wrong NetworkPolicy Direction (Ingress vs Egress)

What Goes Wrong

The question says “prevent pods in namespace secure from sending traffic to external IPs.” You create an ingress deny policy instead of an egress deny policy. Or vice versa — the question asks to block incoming traffic and you block outgoing.

This is the most common mistake on the CKS exam.

How to Avoid It

Think about it from the pod’s perspective:

Direction Meaning Think of It As
Ingress Traffic coming IN to the pod “Who can talk to me?”
Egress Traffic going OUT from the pod “Who can I talk to?”

Before writing the policy, say out loud (or in your head): “I am the pod. Am I blocking what comes IN or what goes OUT?”

Quick Fix

If you applied the wrong direction:

# Delete the wrong policy
k delete networkpolicy wrong-policy -n secure

# Fix the policyTypes and rules, then reapply
k apply -f correct-policy.yaml

Mistake 3: Incomplete Seccomp Profile Path

What Goes Wrong

You create a Seccomp profile and place it in a random directory. The Pod spec references the profile, but kubelet cannot find it because Seccomp profiles must be under /var/lib/kubelet/seccomp/.

How to Avoid It

Always place custom Seccomp profiles in the correct directory:

# On the node
ssh node01
mkdir -p /var/lib/kubelet/seccomp/profiles
cp my-profile.json /var/lib/kubelet/seccomp/profiles/my-profile.json
exit

In the Pod spec:

securityContext:
  seccompProfile:
    type: Localhost
    localhostProfile: profiles/my-profile.json

The localhostProfile is relative to /var/lib/kubelet/seccomp/. So profiles/my-profile.json resolves to /var/lib/kubelet/seccomp/profiles/my-profile.json.

Quick Fix

SSH to the node, move the file to the correct path, delete and recreate the Pod.


Mistake 4: Not Verifying Falco Rules Are Active After Editing

What Goes Wrong

You edit a Falco rules file to add a custom rule, but you do not restart Falco or use hot-reload. The rule never takes effect and you score zero for that question.

How to Avoid It

After editing any Falco rules file:

# Option 1: Restart Falco
systemctl restart falco

# Option 2: Hot-reload (send SIGHUP)
kill -1 $(pidof falco)

# Verify Falco is running with your rules
systemctl status falco
falco --list | grep your-rule-name

Quick Fix

Restart Falco and verify the rule appears in the loaded rules list.


Mistake 5: Missing Namespace in RBAC Bindings

What Goes Wrong

The question says “Create a Role and RoleBinding in namespace dev.” You create the Role in dev but forget the -n dev flag on the RoleBinding, so it is created in namespace default.

A Role in dev with a RoleBinding in default = the binding has no effect.

How to Avoid It

Always specify the namespace explicitly for both Role and RoleBinding:

k create role pod-reader --verb=get,list --resource=pods -n dev
k create rolebinding pod-reader-binding --role=pod-reader \
  --serviceaccount=dev:mysa -n dev

Or set the namespace context first:

kn dev
k create role pod-reader --verb=get,list --resource=pods
k create rolebinding pod-reader-binding --role=pod-reader \
  --serviceaccount=dev:mysa

Quick Fix

# Check where the binding was created
k get rolebinding pod-reader-binding -n default
# Delete from wrong namespace
k delete rolebinding pod-reader-binding -n default
# Recreate in correct namespace
k create rolebinding pod-reader-binding --role=pod-reader \
  --serviceaccount=dev:mysa -n dev

Mistake 6: Forgetting to Re-Encrypt Existing Secrets After Enabling Encryption at Rest

What Goes Wrong

You configure EncryptionConfiguration for the API server and add the --encryption-provider-config flag. New secrets are encrypted, but existing secrets remain unencrypted in etcd.

How to Avoid It

After enabling encryption at rest, re-encrypt all existing secrets:

kubectl get secrets --all-namespaces -o json | kubectl replace -f -

This reads every secret through the API server (which decrypts it) and writes it back (which re-encrypts it with the new provider).

Quick Fix

Run the above command. Verify with etcdctl that the secret data is now encrypted:

ETCDCTL_API=3 etcdctl get /registry/secrets/default/my-secret \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt

If the output is garbled/binary (starts with k8s:enc:aescbc:v1:), encryption is working.


Mistake 7: Not Checking Which Cluster/Context You Are On

What Goes Wrong

You execute commands on the wrong cluster. The question says “Switch to context cluster2-admin@cluster2” but you forget to run the switch command and apply changes to cluster1. You get zero points for that question AND may break another question’s environment.

How to Avoid It

Make this a ritual. Before working on EVERY question:

# 1. Read which context the question specifies
# 2. Switch to it
kubectl config use-context <context-from-question>

# 3. Verify
kubectl config current-context

Quick Fix

If you applied a resource to the wrong cluster:

# Switch to the wrong cluster
kubectl config use-context <wrong-cluster>
# Delete the resource you mistakenly created
kubectl delete <resource> <name> -n <namespace>
# Switch to the correct cluster
kubectl config use-context <correct-cluster>
# Recreate the resource
kubectl apply -f resource.yaml

Mistake 8: Forgetting DNS Egress After Default Deny Egress

What Goes Wrong

You apply a default deny egress NetworkPolicy. All egress is blocked — including DNS queries to CoreDNS. Now pods cannot resolve service names, and every other network operation fails silently with DNS resolution errors.

How to Avoid It

Whenever you create a default deny egress policy, always create a companion DNS allow policy:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: secure
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Quick Fix

Apply the DNS allow policy above and verify:

kubectl exec <pod> -n secure -- nslookup kubernetes

Mistake 9: Using Wrong AppArmor Annotation Syntax

What Goes Wrong

You write:

annotations:
  apparmor.security.beta.kubernetes.io/secure: localhost/my-profile

The correct key format is:

annotations:
  container.apparmor.security.beta.kubernetes.io/<container-name>: localhost/<profile-name>

Note the container. prefix and that <container-name> must match the actual container name from the Pod spec, not the Pod name.

How to Avoid It

Always reference the Kubernetes AppArmor docs. The annotation format is:

container.apparmor.security.beta.kubernetes.io/<CONTAINER_NAME>: localhost/<PROFILE_NAME>

Double-check:

  1. The key starts with container.apparmor.security.beta.kubernetes.io/
  2. The container name matches .spec.containers[].name
  3. The value is localhost/<profile-name> where <profile-name> matches what aa-status shows

Quick Fix

Edit the Pod annotation:

k delete pod <pod-name>
# Fix annotation in YAML
vi pod.yaml
k apply -f pod.yaml

Mistake 10: Not Restarting API Server After Modifying Static Pod Manifest

What Goes Wrong

You modify /etc/kubernetes/manifests/kube-apiserver.yaml but the change does not take effect. In most environments, the kubelet watches this directory and automatically restarts the static pod. However, sometimes:

  • The kubelet takes 30–60 seconds to detect the change
  • If you introduced a YAML error, the API server crashes and does not come back
  • If you are editing on the wrong node, nothing happens

How to Avoid It

After editing the API server manifest:

# 1. Wait for the API server to restart
watch crictl ps

# 2. Or watch for the kube-apiserver pod
watch kubectl get pods -n kube-system

# 3. If it does not come back after 60 seconds, check for errors
crictl logs $(crictl ps -a | grep kube-apiserver | head -1 | awk '{print $1}')

Quick Fix

If the API server is down due to a bad edit:

# Restore from your backup
cp /etc/kubernetes/manifests/kube-apiserver.yaml.bak \
   /etc/kubernetes/manifests/kube-apiserver.yaml
# Wait for recovery
watch crictl ps

Always back up before editing:

cp /etc/kubernetes/manifests/kube-apiserver.yaml{,.bak}

Mistake 11: Forgetting to Mount the Audit Policy File in kube-apiserver

What Goes Wrong

You create an audit policy at /etc/kubernetes/audit/policy.yaml and add the flag --audit-policy-file=/etc/kubernetes/audit/policy.yaml to the API server. But you forget to add the volume and volumeMount to make the host file accessible inside the API server container.

The API server fails to start because it cannot find the file.

How to Avoid It

You need three things for audit policy:

# 1. The command flag
spec:
  containers:
    - command:
        - kube-apiserver
        - --audit-policy-file=/etc/kubernetes/audit/policy.yaml
        - --audit-log-path=/var/log/kubernetes/audit.log
        - --audit-log-maxage=30
        - --audit-log-maxbackup=10

      # 2. Volume mounts
      volumeMounts:
        - name: audit-policy
          mountPath: /etc/kubernetes/audit
          readOnly: true
        - name: audit-log
          mountPath: /var/log/kubernetes
          readOnly: false

  # 3. Volumes
  volumes:
    - name: audit-policy
      hostPath:
        path: /etc/kubernetes/audit
        type: DirectoryOrCreate
    - name: audit-log
      hostPath:
        path: /var/log/kubernetes
        type: DirectoryOrCreate

All three are required: flag + volumeMount + volume. Missing any one of them means audit logging will not work.

Quick Fix

Edit the API server manifest and add the missing volume/volumeMount sections. Wait for restart.


Mistake 12: Not Specifying policyTypes in NetworkPolicy

What Goes Wrong

You create a NetworkPolicy without a policyTypes field and without any ingress/egress rules:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: secure
spec:
  podSelector: {}

This is effectively a no-op. Without policyTypes and without rules, the policy does nothing.

How to Avoid It

Always explicitly specify policyTypes:

# Default deny ingress
spec:
  podSelector: {}
  policyTypes:
    - Ingress

# Default deny egress
spec:
  podSelector: {}
  policyTypes:
    - Egress

# Default deny both
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

Quick Fix

Edit the NetworkPolicy to include the policyTypes field:

k edit networkpolicy default-deny -n secure
# Add policyTypes under spec

Mistake 13: Creating ClusterRole When Role Was Needed

What Goes Wrong

The question says “Create permissions for ServiceAccount app-sa to get pods in namespace dev.” You create a ClusterRole + ClusterRoleBinding, which gives permissions across all namespaces — a security violation that may cost you points or break other questions.

How to Avoid It

Question Says You Should Create
“in namespace X” Role + RoleBinding (both in namespace X)
“across all namespaces” ClusterRole + ClusterRoleBinding
“in multiple specific namespaces” ClusterRole + RoleBinding in each namespace

Read the question carefully for namespace scope.

Quick Fix

# Delete the overly permissive resources
k delete clusterrolebinding app-sa-binding
k delete clusterrole app-sa-role

# Create namespace-scoped resources
k create role app-sa-role --verb=get --resource=pods -n dev
k create rolebinding app-sa-binding --role=app-sa-role \
  --serviceaccount=dev:app-sa -n dev

Mistake 14: Not Testing Changes After Applying Them

What Goes Wrong

You create a NetworkPolicy, RBAC role, or security context and move on to the next question without verifying it works. Later, you find out the policy had a typo in a label selector and was not selecting any pods.

How to Avoid It

After every resource creation, run a quick verification:

Resource Verification Command
NetworkPolicy k exec <pod> -- wget -qO- --timeout=2 <target>
RBAC Role/Binding k auth can-i get pods --as=system:serviceaccount:<ns>:<sa> -n <ns>
AppArmor k get pod <pod> -o yaml \| grep apparmor + check Pod is Running
Seccomp k get pod <pod> -o yaml \| grep seccomp + check Pod is Running
Audit Policy cat /var/log/kubernetes/audit.log \| tail -5
Encryption at Rest etcdctl get /registry/secrets/... should show encrypted data
Falco Rule falco -r /etc/falco/falco_rules.local.yaml --list \| grep rule-name

Spend 30 seconds verifying. It is worth 10x that in points saved.

Quick Fix

If verification fails, debug immediately — do not move on hoping it will work.


Mistake 15: Running Out of Time on Hard Questions

What Goes Wrong

You encounter an ImagePolicyWebhook or complex OPA Gatekeeper question as question 3 of 16. You spend 20 minutes trying to get it working perfectly. Now you have 100 minutes for 13 questions, some of which are easy — but you are stressed, rushing, and making more mistakes.

How to Avoid It

Follow the 3-pass strategy from the Exam Format & Strategy section:

  1. Pass 1 (15–20 min): Only solve questions you can finish in under 3 minutes
  2. Pass 2 (60–70 min): Solve medium-difficulty questions (5–8 min each)
  3. Pass 3 (remaining): Attempt hard questions

The 8-minute rule: If you have been on a single question for 8 minutes without progress, stop and move on.

Quick Fix

If you realize you have spent too long on a question mid-exam:

  1. Save whatever partial work you have (it might earn partial credit)
  2. Move to the next question immediately
  3. Flag the question for later review
  4. Come back in Pass 3 only if you have time remaining

Summary: Pre-Submit Checklist

Before moving on from any question, run through this mental checklist:

  • Am I on the correct cluster/context?
  • Did I create the resource in the correct namespace?
  • Did I test/verify the result?
  • For NetworkPolicy: did I specify policyTypes?
  • For RBAC: Role vs ClusterRole — is the scope correct?
  • For AppArmor: is the profile loaded on the node?
  • For audit: did I add all three (flag + volumeMount + volume)?
  • For Seccomp: is the profile in /var/lib/kubelet/seccomp/?
  • For encryption: did I re-encrypt existing secrets?

Practice Questions

Question 1: You applied a default deny egress NetworkPolicy to the `web` namespace. All pods can no longer resolve DNS names. Write the NetworkPolicy that fixes DNS resolution while keeping all other egress denied.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: web
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

This allows all pods in the web namespace to send DNS queries to CoreDNS on port 53 (UDP and TCP). All other egress remains denied by the default deny policy.

Verification:

kubectl exec -n web <any-pod> -- nslookup kubernetes
# Should resolve successfully
Question 2: You added `--audit-policy-file=/etc/kubernetes/audit/policy.yaml` to the API server manifest, but the API server is crash-looping. What are the three most likely causes?
  1. Missing volume mount: The API server container does not have access to /etc/kubernetes/audit/policy.yaml because you did not add a volumeMount and volume entry.

  2. Invalid audit policy YAML: The policy file has a syntax error (bad indentation, missing fields, wrong API version). Validate with:
    python3 -c "import yaml; yaml.safe_load(open('/etc/kubernetes/audit/policy.yaml'))"
    
  3. File does not exist at the path: You specified /etc/kubernetes/audit/policy.yaml in the flag but the file is at a different location (e.g., /etc/kubernetes/audit-policy.yaml).

Debug steps:

# Check API server container logs
crictl logs $(crictl ps -a | grep kube-apiserver | head -1 | awk '{print $1}')

# Verify the file exists
ls -la /etc/kubernetes/audit/policy.yaml

# Restore backup if needed
cp /etc/kubernetes/manifests/kube-apiserver.yaml.bak \
   /etc/kubernetes/manifests/kube-apiserver.yaml
Question 3: You created a Role `pod-reader` in namespace `dev` and a RoleBinding `pod-reader-binding` in namespace `default`. The ServiceAccount `dev:app-sa` still cannot read pods in `dev`. Why?

The RoleBinding is in the wrong namespace. A RoleBinding in namespace default binds permissions for namespace default, not dev. Even though the Role pod-reader exists in dev, the RoleBinding must also be in dev to take effect.

Fix:

# Delete the binding in the wrong namespace
kubectl delete rolebinding pod-reader-binding -n default

# Create the binding in the correct namespace
kubectl create rolebinding pod-reader-binding \
  --role=pod-reader \
  --serviceaccount=dev:app-sa \
  -n dev

# Verify
kubectl auth can-i get pods \
  --as=system:serviceaccount:dev:app-sa \
  -n dev
# Should return "yes"
Question 4: You configured encryption at rest with aescbc and verified that new Secrets are encrypted. However, when you check an old Secret in etcd, it is still in plaintext. What did you forget?

You forgot to re-encrypt existing Secrets. Enabling encryption at rest only encrypts newly written data. Existing Secrets remain in their previous format (plaintext) until they are re-written.

Fix:

# Re-encrypt all existing Secrets
kubectl get secrets --all-namespaces -o json | kubectl replace -f -

This reads every Secret through the API server (which returns decrypted data) and writes it back through the API server (which now encrypts it with the aescbc provider).

Verify:

ETCDCTL_API=3 etcdctl get /registry/secrets/default/<secret-name> \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt

# Output should start with "k8s:enc:aescbc:v1:" indicating encryption
Question 5: You created a Pod with an AppArmor annotation `container.apparmor.security.beta.kubernetes.io/web: localhost/deny-write`. The Pod is stuck in Pending. What steps do you take to debug?

Step-by-step debugging:

# 1. Check Pod status and events
kubectl describe pod <pod-name>
# Look for events mentioning AppArmor

# 2. Check the container name in the annotation
kubectl get pod <pod-name> -o yaml | grep -A5 containers
# Verify the container is actually named "web"

# 3. SSH to the target node and check if the profile is loaded
ssh <node-name>
aa-status | grep deny-write

# 4. If the profile is NOT loaded:
apparmor_parser -r /etc/apparmor.d/deny-write

# 5. Verify after loading
aa-status | grep deny-write
# Should show "deny-write" in the list

# 6. Exit the node
exit

# 7. Delete and recreate the Pod
kubectl delete pod <pod-name> --force --grace-period=0
kubectl apply -f pod.yaml

# 8. Verify it is running
kubectl get pod <pod-name>
# Should be in Running state

Common causes for this issue:

  • AppArmor profile not loaded on the node where the Pod is scheduled
  • Typo in the profile name (annotation says deny-write, profile is loaded as deny_write)
  • Container name in annotation does not match .spec.containers[].name

Updated: