TT Lab
Get started
Learn Learning paths Courses

CKS — Kubernetes Security Specialist

CKS Mock Exam A

Continue in TT Lab

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)

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.

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

  1. Create the namespace prod and a NetworkPolicy default-deny that denies both ingress and egress by default for all Pods in it. Put no allow rules at all.
  2. In the namespace prod, create a NetworkPolicy deny-metadata that prevents Pods with the app=web label from reaching the node metadata endpoint 169.254.169.254/32, while leaving all other egress open.
  3. Using a self-signed certificate with CN=shop.internal, create a TLS Secret web-tls in the namespace prod, and create an Ingress web that terminates TLS for the host shop.internal with that Secret. The backend is port 80 of the Service web.

Cluster Hardening

  1. In the namespace prod, create a service account report-runner, and create a Role pod-reader and a RoleBinding report-runner-pod-reader so that this account can only get, list, and watch Pods within prod. Grant no other permissions.
  2. Make the default service account in the namespace prod not automatically mount its token. And create a Pod frontend in prod that uses the service account report-runner, and turn off automatic token mounting in the Pod spec as well.
  3. Create a ClusterRole security-auditor and a ClusterRoleBinding security-auditor so that the security auditor group security-audit can only read Pods, namespaces, and network policies cluster-wide. Grant no write permissions or Secret access.

System Hardening

  1. Write a seccomp profile in /root/exam/seccomp/audit.json. The default action is SCMP_ACT_ERRNO, and you must open at least a few system calls with SCMP_ACT_ALLOW. And create a Pod probe in the namespace prod and make it use this profile in the Localhost way at the path profiles/audit.json.
  2. Write an AppArmor profile in /root/exam/apparmor/k8s-deny-write. The profile name is k8s-deny-write, and it must have the line #include <tunables/global> and the rule deny /** w, that blocks all writes. And create a Deployment logshipper in the namespace prod and make the Pod template's securityContext.appArmorProfile use this profile as Localhost.

Minimize Microservice Vulnerabilities

  1. Create the namespace payments, apply all three Pod security admission modes enforce, audit, and warn as restricted, and pin the versions of all three modes to latest.
  2. Try applying a Pod bad-pod that violates restricted in the namespace payments, and save the rejected response, including standard error, to /root/exam/denied.txt. The Pod must not actually be created.
  3. Create a RuntimeClass gvisor (handler runsc), and create a Deployment checkout with 2 replicas in the namespace payments. The Pod template must use this RuntimeClass and pass restricted. The container name is app; put runAsNonRoot: true and seccompProfile.type: RuntimeDefault at the Pod level, and allowPrivilegeEscalation: false, capabilities.drop: [ALL], and readOnlyRootFilesystem: true at the container level.

Supply Chain Security

  1. Create the namespace supply and a Deployment payments-api. The container name is api, and the image must be pinned by digest, not by tag. The value is registry.internal/payments-api@sha256:140eab0459241fb1643767dd4cc3576d282b0059a6328521e714cb545fa4adea, and imagePullPolicy is IfNotPresent.
  2. 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 every FROM must be pinned with an @sha256: digest, and build outputs must be brought in only with COPY --from=. Don't use ADD, the last USER must be a numeric UID other than 0, and don't leave names that look like secrets in ENV or ARG.
  3. Block images from unapproved registries. Create a ValidatingAdmissionPolicy trusted-images and a ValidatingAdmissionPolicyBinding trusted-images, and have them require that all container images start with registry.internal/. The binding's validationActions is Deny, and the scope of application is only the namespace supply. Other namespaces must not be affected.

Monitoring, Logging and Runtime Security

  1. Write an audit policy in /root/exam/audit/policy.yaml. apiVersion is audit.k8s.io/v1, kind is Policy, and the top-level omitStages is the single entry RequestReceived. There are exactly three rules and order matters. First, record secrets and configmaps of the core group at the Metadata level. Second, record pods/exec, pods/attach, and pods/portforward at the RequestResponse level. Third, catch everything else with a Metadata rule that lists no targets.
  2. Write a kube-apiserver static Pod manifest with audit logging turned on in /root/exam/audit/kube-apiserver.yaml. The first container name is kube-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 volumes audit-policy and audit-logs respectively, with the policy mount readOnly: true and the log mount not read-only.
  3. Create a Deployment ledger in the namespace prod. The container name is app, with readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, and capabilities.drop: [ALL]. Provide /tmp, which needs writing, as an emptyDir volume scratch, and don't use a hostPath volume or hostNetwork, hostPID, or hostIPC.

Notes

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.