CKS — Common Mistakes
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:
- The key starts with
container.apparmor.security.beta.kubernetes.io/ - The container name matches
.spec.containers[].name - The value is
localhost/<profile-name>where<profile-name>matches whataa-statusshows
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:
- Pass 1 (15–20 min): Only solve questions you can finish in under 3 minutes
- Pass 2 (60–70 min): Solve medium-difficulty questions (5–8 min each)
- 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:
- Save whatever partial work you have (it might earn partial credit)
- Move to the next question immediately
- Flag the question for later review
- 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?
-
Missing volume mount: The API server container does not have access to
/etc/kubernetes/audit/policy.yamlbecause you did not add avolumeMountandvolumeentry. - 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'))" - File does not exist at the path: You specified
/etc/kubernetes/audit/policy.yamlin 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 asdeny_write) - Container name in annotation does not match
.spec.containers[].name