CKS study materials
CKS study resources

CKS common mistakes

CKS · Understand the traps and how to avoid them.

Mistake 1

Applying a default-deny NetworkPolicy to a namespace and then only adding allow rules for the app traffic, leaving pods unable to resolve DNS. Pods start fine but every service call fails with 'no such host'.

Once a pod is selected by any egress-isolating policy, all egress including DNS to kube-dns/CoreDNS is dropped. Candidates focus on the app ports in the task and forget that name resolution is also egress.

What to do instead

Add an explicit egress rule allowing UDP and TCP port 53 to the DNS pods (namespaceSelector matching kube-system plus podSelector matching k8s-app=kube-dns), or to the cluster DNS Service IP on port 53. Verify with kubectl exec -- nslookup kubernetes.default from a pod actually selected by the policy, not from the node.

Mistake 2

Combining namespaceSelector and podSelector inside one element of a policy's 'from'/'to' list and expecting OR behaviour, so traffic from the intended pods in other namespaces is still blocked (or traffic from everywhere is allowed by an empty podSelector: {}).

Peers in the list are ORed, but selectors within one peer are ANDed. It reads like a natural sentence ('from namespace X and pods labelled Y') and the resulting over-restriction only shows up at test time.

What to do instead

Put namespaceSelector and podSelector in two separate list items when OR is intended; keep them in one item when you want pods with that label inside that namespace. After applying, test both a positive case (should connect) and a negative case (should time out).

Mistake 3

Creating an Ingress with a TLS section whose secretName does not exist, or creating the TLS secret with keys other than tls.crt/tls.key, or with a certificate whose CN/SAN does not match the Ingress host. HTTPS either serves the default certificate or fails.

Ingress controllers silently fall back when the secret is missing or malformed, so the resource looks correct in kubectl get ingress. Candidates also hand-craft secrets from crt/key files with wrong key names.

What to do instead

Use kubectl create secret tls --cert=tls.crt --key=tls.key in the same namespace as the Ingress, generate the cert with a SAN matching the host from the rule, and reference it under spec.tls[]. Verify with curl -k --resolve :443: https:// and inspect the served certificate subject.

Mistake 4

Granting permissions to a ServiceAccount with a RoleBinding created in the wrong namespace, or omitting the namespace in the subject, so the intended ServiceAccount gets nothing while a same-named SA in the default namespace is bound.

The subject entry system:serviceaccount:: is easy to mistype, and a RoleBinding placed in namespace A can never grant access in namespace B. The failure is silent; nothing errors at apply time.

What to do instead

Create the binding in the same namespace as the workload, use kubectl create rolebinding --clusterrole= --serviceaccount=: -n , and always verify with kubectl auth can-i --as=system:serviceaccount:: -n for both a permitted and a forbidden verb.

Mistake 5

Editing the kubelet configuration to disable anonymous authentication and enable webhook authorization, but editing /etc/kubernetes/kubelet.conf, or editing /var/lib/kubelet/config.yaml without restarting the kubelet, then reporting success without checking port 10250.

Candidates memorise the flags (--anonymous-auth=false) but on modern clusters the kubelet reads a config file whose path is given on the systemd unit; without systemctl restart kubelet the change never takes effect, and on some setups the flag is ignored anyway.

What to do instead

Set authentication.anonymous.enabled: false and authorization.mode: Webhook in /var/lib/kubelet/config.yaml (and remove readOnlyPort), run systemctl daemon-reload && systemctl restart kubelet, then prove it from the node with curl -k https://localhost:10250/pods returning 401/403 rather than a pod list.

Mistake 6

Referencing a seccomp profile with a full host path or with the wrong type, or applying an AppArmor annotation for a profile that was never loaded on the node, so the pod is rejected or silently runs with the default profile.

The profile must live under the kubelet's seccomp root (by default /var/lib/kubelet/seccomp) and be referenced as a relative localhostProfile, which is counter-intuitive. AppArmor additionally requires the profile to be loaded on the node and, depending on version, the field versus the legacy annotation.

What to do instead

Place the JSON under /var/lib/kubelet/seccomp//.json and use seccompProfile: {type: Localhost, localhostProfile: /.json} (or RuntimeDefault). For AppArmor, load the profile on the node with apparmor_parser -r and use the supported field/annotation. Verify inside the container with cat /proc/1/attr/current and kubectl exec ... -- grep Seccomp /proc/1/status.

Mistake 7

Building a hardened Pod without first labelling the namespace for Pod Security Admission, so the workload is admitted under the permissive default; or labelling with an invalid mode such as 'strict' or putting the label on the Pod instead of the Namespace.

PSA is namespace-scoped and only accepts privileged/baseline/restricted. Candidates write the secure pod, see it scheduled, and assume enforcement is on; the admission rejection only appears when the namespace label is actually correct.

What to do instead

Label the namespace, e.g. kubectl label ns pod-security.kubernetes.io/enforce=restricted pod-security.kubernetes.io/enforce-version=latest, and pre-check with kubectl label --dry-run=server --overwrite ns pod-security.kubernetes.io/enforce=restricted. Confirm the pod satisfies restricted: seccompProfile RuntimeDefault, runAsNonRoot, allowPrivilegeEscalation: false, capabilities drop ALL.

Mistake 8

Hardening a container with runAsNonRoot: true and no numeric runAsUser, or with readOnlyRootFilesystem: true and no writable volumes, so the container never starts (CreateContainerError / CrashLoopBackOff) and the task is left broken.

runAsNonRoot requires the image to declare a non-root USER or an explicit runAsUser; many default images run as root. Many apps and entrypoints also write to /tmp, /var/run or a cache directory at startup, which a read-only root filesystem blocks.

What to do instead

Pair runAsNonRoot: true with an explicit runAsUser/runAsGroup, and add emptyDir volumes with volumeMounts for every path the app writes (/tmp, /var/run, /var/cache/). Check with kubectl describe pod and kubectl logs immediately after applying rather than assuming success.

Mistake 9

Enabling the ImagePolicyWebhook admission plugin in the kube-apiserver static pod without adding the hostPath volume and volumeMount for the admission configuration file (and the webhook kubeconfig), leaving the control plane's API server in CrashLoopBackOff.

The plugin is configured entirely through a static manifest plus mounted files; candidates add --enable-admission-plugins and --admission-control-config-file but forget that a static pod cannot see host files unless they are mounted, and forget to keep the existing plugins such as NodeRestriction.

What to do instead

Append the plugin to the existing --enable-admission-plugins list, add hostPath volumes plus volumeMounts for the AdmissionConfiguration file and the webhook kubeconfig it references, and use defaultAllow: false with an explicit allowlist. Within a minute confirm the API server is healthy (kubectl get --raw /readyz or crictl ps on the control-plane node) and be ready to revert the manifest.

Mistake 10

Writing an audit policy and adding --audit-policy-file and --audit-log-path to the kube-apiserver static pod, but not creating/mounting the log directory, or leaving the policy with no rules matching the target verbs, so no events are recorded.

The static pod cannot write to an arbitrary host path or read a policy file that is not mounted; and an audit policy is ordered with the first matching rule winning, so an early broad rule at level None silently swallows the events the task asks for.

What to do instead

Mount the policy file and the log directory as hostPath volumes into the kube-apiserver container, set apiVersion audit.k8s.io/v1 with ordered rules (omitStages RequestReceived as needed), restart via manifest change, and verify by generating the audited action and checking tail/grep of the audit log on the control-plane node.