CKS — Kubernetes Security Specialist
CKS Mock Exam A
Goal
You solve 17 tasks within 120 minutes under the same conditions as the real CKS. The passing score is 67%, and since it uses partial credit, passing 12 of the 17 counts as complete.
This is a practice exam. Don't look at the hints or the answer key; try to finish it through to the end first. It is better to mark a task you are stuck on, move past it, and come back in the remaining time. You can press grading at any time, and pressing it several times doesn't change the result.
Why it matters
The CKS is the only exam with passing the CKA as a prerequisite. It assumes you already have the hands to work with Kubernetes, so the questions are closer to "find what is dangerous in this configuration and fix it" than to "create this." Defense and diagnosis are what is tested, and attack techniques are not covered.
It is the same in practice. The places where clusters get breached are usually not newly built features but places where defaults were left as they were. Pod security admission isn't applied to the namespace, the default service account token is mounted into every Pod, and images are deployed with a moving tag. What this exam asks about is exactly that list.
Exam environment (facts confirmed in the real exam)
- The documents you can view are
kubernetes.io/docs,kubernetes.io/blog,falco.org/docs,kubernetes-sigs.github.io/bom,etcd.io/docs,kubernetes.github.io/ingress-nginx,docs.cilium.io, andistio.io/latest/docs. - Searching within the documentation sites is allowed, but results must not lead outside the allowlist.
- The
kalias and bash autocompletion are already set up. Don't spend time creating aliases. - You work by using
sshto the host designated for each task, and nested ssh isn't supported.exitbefore moving on to the next task. - The tools exist only on the host you connect to over SSH.
- Copy is
Ctrl+Shift+Cand paste isCtrl+Shift+V.
What is different in this practice exam environment
This lab's cluster is a single-user cluster that runs inside a Pod. The kube-apiserver is real, so manifests, RBAC, Pod security admission, and admission policies really work and really reject. However, there is no runtime that actually runs containers, so the following two things can't be confirmed.
- NetworkPolicy is applied, but no actual blocking happens. This is because there is no CNI. Tasks 1 and 2 look only at whether the policy objects are correct.
- seccomp and AppArmor profiles are not loaded into the kernel. Tasks 7 and 8 look at the contents of the profile files and the wiring attached to the Pod.
Everything else is recomputed and graded on the live cluster. Permissions are asked again with
kubectl auth can-i, and admission policies are asked again with a server dry-run for the verdict at that moment.
Steps
Cluster Setup
- Create the namespace
prodand a NetworkPolicydefault-denythat denies both ingress and egress by default for all Pods in it. Put no allow rules at all. - In the namespace
prod, create a NetworkPolicydeny-metadatathat prevents Pods with theapp=weblabel from reaching the node metadata endpoint169.254.169.254/32, while leaving all other egress open. - Using a self-signed certificate with
CN=shop.internal, create a TLS Secretweb-tlsin the namespaceprod, and create an Ingresswebthat terminates TLS for the hostshop.internalwith that Secret. The backend is port 80 of the Serviceweb.
Cluster Hardening
- In the namespace
prod, create a service accountreport-runner, and create a Rolepod-readerand a RoleBindingreport-runner-pod-readerso that this account can onlyget,list, andwatchPods withinprod. Grant no other permissions. - Make the
defaultservice account in the namespaceprodnot automatically mount its token. And create a Podfrontendinprodthat uses the service accountreport-runner, and turn off automatic token mounting in the Pod spec as well. - Create a ClusterRole
security-auditorand a ClusterRoleBindingsecurity-auditorso that the security auditor groupsecurity-auditcan only read Pods, namespaces, and network policies cluster-wide. Grant no write permissions or Secret access.
System Hardening
- Write a seccomp profile in
/root/exam/seccomp/audit.json. The default action isSCMP_ACT_ERRNO, and you must open at least a few system calls withSCMP_ACT_ALLOW. And create a Podprobein the namespaceprodand make it use this profile in theLocalhostway at the pathprofiles/audit.json. - Write an AppArmor profile in
/root/exam/apparmor/k8s-deny-write. The profile name isk8s-deny-write, and it must have the line#include <tunables/global>and the ruledeny /** w,that blocks all writes. And create a Deploymentlogshipperin the namespaceprodand make the Pod template'ssecurityContext.appArmorProfileuse this profile asLocalhost.
Minimize Microservice Vulnerabilities
- Create the namespace
payments, apply all three Pod security admission modesenforce,audit, andwarnasrestricted, and pin the versions of all three modes tolatest. - Try applying a Pod
bad-podthat violatesrestrictedin the namespacepayments, and save the rejected response, including standard error, to/root/exam/denied.txt. The Pod must not actually be created. - Create a RuntimeClass
gvisor(handlerrunsc), and create a Deploymentcheckoutwith 2 replicas in the namespacepayments. The Pod template must use this RuntimeClass and passrestricted. The container name isapp; putrunAsNonRoot: trueandseccompProfile.type: RuntimeDefaultat the Pod level, andallowPrivilegeEscalation: false,capabilities.drop: [ALL], andreadOnlyRootFilesystem: trueat the container level.
Supply Chain Security
- Create the namespace
supplyand a Deploymentpayments-api. The container name isapi, and the image must be pinned by digest, not by tag. The value isregistry.internal/payments-api@sha256:140eab0459241fb1643767dd4cc3576d282b0059a6328521e714cb545fa4adea, andimagePullPolicyisIfNotPresent. - In
/root/exam/Dockerfile, write a Dockerfile that builds a deployment image. It must be a multi-stage build separating a build stage and a run stage, the base of everyFROMmust be pinned with an@sha256:digest, and build outputs must be brought in only withCOPY --from=. Don't useADD, the lastUSERmust be a numeric UID other than 0, and don't leave names that look like secrets inENVorARG. - Block images from unapproved registries. Create a ValidatingAdmissionPolicy
trusted-imagesand a ValidatingAdmissionPolicyBindingtrusted-images, and have them require that all container images start withregistry.internal/. The binding'svalidationActionsisDeny, and the scope of application is only the namespacesupply. Other namespaces must not be affected.
Monitoring, Logging and Runtime Security
- Write an audit policy in
/root/exam/audit/policy.yaml.apiVersionisaudit.k8s.io/v1,kindisPolicy, and the top-levelomitStagesis the single entryRequestReceived. There are exactly three rules and order matters. First, recordsecretsandconfigmapsof the core group at theMetadatalevel. Second, recordpods/exec,pods/attach, andpods/portforwardat theRequestResponselevel. Third, catch everything else with aMetadatarule that lists no targets. - Write a kube-apiserver static Pod manifest with audit logging turned on in
/root/exam/audit/kube-apiserver.yaml. The first container name iskube-apiserver, and it must have the following five flags.--audit-policy-file=/etc/kubernetes/audit/policy.yaml,--audit-log-path=/var/log/kubernetes/audit/audit.log,--audit-log-maxage=30,--audit-log-maxbackup=10,--audit-log-maxsize=100. And mount the hostPath volumesaudit-policyandaudit-logsrespectively, with the policy mountreadOnly: trueand the log mount not read-only. - Create a Deployment
ledgerin the namespaceprod. The container name isapp, withreadOnlyRootFilesystem: true,allowPrivilegeEscalation: false, andcapabilities.drop: [ALL]. Provide/tmp, which needs writing, as anemptyDirvolumescratch, and don't use ahostPathvolume orhostNetwork,hostPID, orhostIPC.
Notes
- When you reapply a label, use
kubectl label ... --overwrite. Without it, you get an error on a label that already exists. - Pod security admission doesn't block the Deployment; it blocks the Pods that Deployment creates.
If the Deployment was created but there are no Pods at all,
look at the reason with
kubectl -n <네임스페이스> describe rs(the placeholder is the namespace). - Don't guess whether the permissions you gave a service account are right; check with
kubectl auth can-i <동사> <자원> --as=system:serviceaccount:<ns>:<이름>(the placeholders are the verb, the resource, and the name). You simulate a group with--as-group. - Two common mistakes. If you don't write
policyTypesin a NetworkPolicy, egress isn't controlled. AndautomountServiceAccountTokenexists on both the service account and the Pod, and the Pod side wins.
Default deny for the prod namespace
If you leave a NetworkPolicy's podSelector as an empty object, it selects all Pods in that namespace. And you must list both Ingress and Egress in policyTypes to block both directions. If you don't write the rules (ingress/egress) at all, it means "there is nothing to allow."
Block node metadata
ipBlock opens a range with cidr and carves out part of it with except. To allow everything while blocking a single address, the cidr is 0.0.0.0/0 and you write that address in except. This task deals only with Egress.
A TLS-terminating Ingress
Create the key and certificate in one go with openssl req -x509 -nodes, and put them in with kubectl create secret tls. The Ingress must list the host and secretName in spec.tls for it to terminate that host with that certificate. It must be the same name as the host in spec.rules.
Least privilege for a service account
A Role takes effect only inside its namespace. Verbs you don't list in verbs aren't allowed, so you only need to write get, list, and watch. The RoleBinding's subject is specified in the format --serviceaccount=:. Grading checks the boundary from both sides with kubectl auth can-i.
Block automatic token mounting
automountServiceAccountToken exists on both the service account and the Pod, and the Pod side wins. You turn it off on the service account side with kubectl patch serviceaccount, and for the Pod you write it directly in the spec. Both places must be false.
A read-only cluster auditor
Permissions that cross namespaces are given with a ClusterRole, not a Role, and bound with a ClusterRoleBinding. The subject kind is Group, not ServiceAccount, and the apiGroup is rbac.authorization.k8s.io. networkpolicies is in the networking.k8s.io group, not the core group.
Write a seccomp profile and wire it up
A seccomp profile sets the default verdict with defaultAction and opens exceptions in the syscalls array. If the default is deny (SCMP_ACT_ERRNO), there must be an allow list. In the Pod, set the type of securityContext.seccompProfile to Localhost and write in localhostProfile the path relative to the node's seccomp root.
Write an AppArmor profile and wire it up
An AppArmor profile is a profile { ... } block with access rules inside it. A rule that blocks all writes starts with deny and ends with a path pattern, permission characters, and a comma. On the Pod side, use the securityContext.appArmorProfile field. Write it with the field, not the old annotation approach.
Pod security admission restricted
Pod security admission is turned on with namespace labels. There are three modes, enforce, audit, and warn, and you write the level in the pod-security.kubernetes.io/ label for each. The version is a label with -version added to the same prefix. That makes six labels.
Confirm that a violating Pod is rejected
The rejection message comes out on standard error. If you forget 2>&1 in the redirection, an empty file is left. You don't need to work hard to make a violating Pod; just write nothing at all in securityContext. restricted requires four things at once, so it gets caught as is.
A sandbox runtime workload
A RuntimeClass is a cluster-scoped resource and is created with just a handler. In the Pod template, you point to it with runtimeClassName. restricted requires four things, two written at the Pod level and two at the container level. If the Deployment was created but there are no Pods, it got caught by restricted.
Pin the image by digest
A tag moves and a digest doesn't. When pinning by digest, join the name and value with an at sign (@), not a colon, and don't write a tag together with it. If you pin by digest, there is no reason to set imagePullPolicy to Always.
Image build hygiene
Multi-stage means using FROM two or more times, naming the earlier stage with AS, and bringing in only the build outputs with COPY --from=. Pin the base by digest too, not by tag. ADD fetches remote resources and unpacks archives automatically, so it is hard to predict what will come in. If you write a name in USER, that name must exist in the image, so a numeric UID is safer.
Enforce trusted registries
A ValidatingAdmissionPolicy makes its verdict with a CEL expression, and to actually block, the binding's validationActions must have Deny. If you put only Warn, only a warning goes out and the Pod is created. You narrow the scope with the binding's matchResources. To pick just one namespace, use the kubernetes.io/metadata.name label. This label is attached automatically to every namespace.
Write an audit policy
An audit policy is scanned from the top and stops at the first matching rule. So if you put a catch-all rule first, the finer rules after it are never used. Subresources are written with a slash, like pods/exec. If you put omitStages at the top level, it applies to all rules.
Wire up the audit log
If you write only the flags, the apiserver can't find the policy file. A static Pod can see those paths only if it mounts the node filesystem as a hostPath volume. The policy file only needs to be read while the log directory must be written, so the readOnly of the two mounts differs. If you write a type on the hostPath, it distinguishes files from directories.
Runtime immutability
If you make the root read-only, most applications can't write temporary files and die. So you pick only the paths that need writing and open them as volumes. Mounting the node filesystem as is breaks isolation, so use emptyDir. If you share the host namespaces, the container boundary itself disappears.