Linux Foundation Certified Kubernetes Security Specialist (CKS) - CKS Exam Practice Test

SIMULATION
Documentation
ServiceAccount, Deployment,
Projected Volumes
You must connect to the correct host . Failure to do so may
result in a zero score.
[candidate@base] $ ssh cks000033
Context
A security audit has identified a Deployment improperly handling service account tokens, which could lead to security vulnerabilities.
Task
First, modify the existing ServiceAccount stats-monitor-sa in the namespace monitoring to turn off automounting of API credentials.
Next, modify the existing Deployment stats-monitor in the namespace monitoring to inject a ServiceAccount token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.
Use a Projected Volume named token to inject the ServiceAccount token and ensure that it is mounted read-only.
The Deployment's manifest file can be found at /home/candidate/stats-monitor/deployment.yaml.
Correct Answer:
See the Explanation below for complete solution
Explanation:
1) Connect to correct host
ssh cks000033
sudo -i
export KUBECONFIG=/etc/kubernetes/admin.conf
2) Patch the ServiceAccount to disable automounting
Task: turn off automounting of API credentials for stats-monitor-sa in monitoring.
kubectl -n monitoring patch sa stats-monitor-sa -p '{"automountServiceAccountToken": false}' Verify:
kubectl -n monitoring get sa stats-monitor-sa -o yaml | grep -i automount
3) Edit the Deployment manifest file
Task says to modify the manifest at:
/home/candidate/stats-monitor/deployment.yaml
vi /home/candidate/stats-monitor/deployment.yaml
4) In the Deployment, ensure it uses the ServiceAccount AND inject token via Projected Volume
4.1 Make sure Deployment uses the SA
Under:
spec: -> template: -> spec:
ensure:
serviceAccountName: stats-monitor-sa
(If it already exists, leave it; don't add extra changes beyond requirements.)
4.2 Add a projected volume named token
Under:
spec: -> template: -> spec: -> volumes:
add (or modify existing volume if present) so it is exactly:
- name: token
projected:
sources:
- serviceAccountToken:
path: token
This creates the file token inside the mounted directory, so the final path becomes:
/var/run/secrets/kubernetes.io/serviceaccount/token
4.3 Mount the projected volume read-only at the required location
Under the target container:
spec: -> template: -> spec: -> containers: -> (your container) -> volumeMounts:
Add:
- name: token
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
✅ This satisfies:
Projected volume name: token
Mount path: /var/run/secrets/kubernetes.io/serviceaccount/token (file inside mount) Mounted read-only
4.4 Important: Don't break default token mount behavior
Because you disabled SA automounting at the ServiceAccount level, you must explicitly mount the projected token (done above). That's the whole point of this task.
Save and exit:
:wq
5) Apply the updated Deployment
kubectl -n monitoring apply -f /home/candidate/stats-monitor/deployment.yaml Wait rollout:
kubectl -n monitoring rollout status deployment/stats-monitor
6) Verify the token file exists in the running Pod
Get a pod name:
POD=$(kubectl -n monitoring get pods -l app=stats-monitor -o jsonpath='{.items[0].metadata.name}') echo $POD Check the token file path exists:
kubectl -n monitoring exec -it $POD -- ls -l /var/run/secrets/kubernetes.io/serviceaccount/token Optional: confirm it's mounted read-only (usually shown by mount options):
kubectl -n monitoring exec -it $POD -- mount | grep /var/run/secrets/kubernetes.io/serviceaccount
✅ What the examiner checks
SA stats-monitor-sa has:
automountServiceAccountToken: false
Deployment stats-monitor mounts a projected volume named token
Token file is at:
/var/run/secrets/kubernetes.io/serviceaccount/token
Mount is readOnly: true
If label selector doesn't match (-l app=stats-monitor)
Use:
kubectl -n monitoring get pods
Then set:
POD=<paste-pod-name>
SIMULATION
Context
For testing purposes, the kubeadm provisioned cluster 's API server
was configured to allow unauthenticated and unauthorized access.
Task
First, secure the cluster 's API server configuring it as follows:
. Forbid anonymous authentication
. Use authorization mode Node,RBAC
. Use admission controller NodeRestriction
The cluster uses the Docker Engine as its container runtime . If needed, use the docker command to troubleshoot running containers.
kubectl is configured to use unauthenticated and unauthorized access. You do not have to change it, but be aware that kubectl will stop working once you have secured the cluster .
You can use the cluster 's original kubectl configuration file located at etc/kubernetes/admin.conf to access the secured cluster.
Next, to clean up, remove the ClusterRoleBinding
system:anonymous.
Correct Answer:
See the Explanation below for complete solution
Explanation:
1) SSH to control-plane node
ssh cks000002
sudo -i
2) Edit API Server static pod manifest
API server in kubeadm runs as a static pod.
vi /etc/kubernetes/manifests/kube-apiserver.yaml
3) Apply required API Server security settings
3.1 Forbid anonymous authentication
Find command: section and ensure this line exists:
- --anonymous-auth=false
3.2 Use authorization mode Node,RBAC
Ensure exactly this line exists (and no AlwaysAllow):
- --authorization-mode=Node,RBAC
❌ Remove if present:
- --authorization-mode=AlwaysAllow
3.3 Enable admission controller NodeRestriction
Find --enable-admission-plugins and ensure NodeRestriction is included.
Correct example:
- --enable-admission-plugins=NodeRestriction
If other plugins already exist, append NodeRestriction, e.g.:
- --enable-admission-plugins=NamespaceLifecycle,ServiceAccount,NodeRestriction
4) Save file and let kubelet restart API server
Just save and exit (:wq)
Kubelet will automatically restart the API server pod.
5) Switch kubectl to secured config
Current kubectl will stop working after API server hardening.
export KUBECONFIG=/etc/kubernetes/admin.conf
Verify access:
kubectl get nodes
6) Remove insecure ClusterRoleBinding
Delete system:anonymous binding:
kubectl delete clusterrolebinding system:anonymous
Verify removal:
kubectl get clusterrolebinding | grep anonymous
(no output = correct)
7) Quick validation (optional but fast)
API server flags check:
grep -n "anonymous-auth" /etc/kubernetes/manifests/kube-apiserver.yaml
grep -n "authorization-mode" /etc/kubernetes/manifests/kube-apiserver.yaml grep -n "NodeRestriction" /etc/kubernetes/manifests/kube-apiserver.yaml
SIMULATION
Documentation dockerd
You must connect to the correct host . Failure to do so may result in a zero score.
[candidate@base] $ ssh cks000037
Task
Perform the following tasks to secure the cluster node cks000037 :
Remove user developer from the docker group.
Do not remove the user from any other group.
Reconfigure and restart the Docker daemon to ensure that the socket
file located at /var/run/docker.sock is owned by the group root.
Re-configure and restart the Docker daemon to ensure it does not listen on any TCP port.
After completing your work, ensure the Kubernetes cluster is healthy.
Correct Answer:
See the Explanation below for complete solution
Explanation:
1) Connect to the correct host
ssh cks000037
sudo -i
2) Remove user developer from the docker group ONLY
2.1 Verify current groups (optional but fast)
id developer
2.2 Remove ONLY from docker group
gpasswd -d developer docker
2.3 Verify removal
id developer
✅ docker should not appear; other groups must remain.
3) Reconfigure Docker to secure the socket and disable TCP
Docker config file:
vi /etc/docker/daemon.json
3.1 Set socket group to root and disable TCP listeners
Ensure the file contains exactly these relevant settings (merge with existing JSON if present):
{
"group": "root",
"hosts": ["unix:///var/run/docker.sock"]
}
Important:
"group": "root" → docker.sock owned by group root
"hosts" includes ONLY the unix socket (no tcp://)
If the file already exists with other keys, add/adjust only these keys and keep valid JSON (commas!).
Save and exit:
:wq
4) Restart Docker daemon
systemctl daemon-reload
systemctl restart docker
systemctl status docker --no-pager
5) Verify Docker socket ownership and permissions
ls -l /var/run/docker.sock
Expected:
srw-rw---- 1 root root ...
✅ Owner: root
✅ Group: root
6) Verify Docker is NOT listening on TCP
ss -lntp | grep docker
Expected:
No output (or nothing bound to TCP by dockerd)
Optional double-check:
ps aux | grep dockerd | grep -v grep
Ensure no -H tcp://... flags.
7) Ensure Kubernetes cluster is healthy
7.1 Check node and pods
export KUBECONFIG=/etc/kubernetes/admin.conf
kubectl get nodes
kubectl get pods -A
All nodes should be Ready, core pods Running.
SIMULATION
You must connect to the correct host . Failure to do so may
result in a zero score.
[candidato@base] $ ssh cks000023
Task
Analyze and edit the Dockerfile located at /home/candidate/subtle-bee/build/Dockerfile, fixing one instruction present in the file that is a prominent security/best-practice issue.
Do not add or remove instructions; only modify the one existing instruction with a security/best-practice concern.
Do not build the Dockerfile, Failure to do so may result in running out of storage and a zero score.
Analyze and edit the given manifest file /home/candidate/subtle-bee/deployment.yaml, fixing one fields present in the file that are a prominent security/best-practice issue.
Do not add or remove fields; only modify the one existing field with a security/best-practice concern.
Should you need an unprivileged user for any of the tasks, use user nobody with user ID 65535.
Correct Answer:
See the Explanation below for complete solution
Explanation:
0) Connect to the correct host
ssh cks000023
sudo -i
PART A - Fix ONE prominent Dockerfile security/best-practice issue
1) Open the Dockerfile
vi /home/candidate/subtle-bee/build/Dockerfile
2) Find the "most obvious" security/best-practice problem and modify ONLY THAT ONE instruction Use / search in vi to quickly find candidates:
Candidate 1 (very common): USER root (or no USER but a USER 0)
Search:
/USER
If you see:
USER root
Change that single instruction to:
USER 65535
(or USER nobody if that exact word is already used in the file-but the task explicitly allows UID 65535, so USER 65535 is safest.)
✅ This is one-instruction change and is a top-tier best practice.
Candidate 2 (very common): FROM <image>:latest
Search:
/FROM
If you see something like:
FROM nginx:latest
Change ONLY that line to a pinned tag (example):
FROM nginx:1.25.5
(Any non-latest pinned version is the point. Don't add a digest line; just modify the existing FROM line.) Candidate 3: ADD http://... (remote URL download) Search:
/ADD
If you see remote URL usage like:
ADD https://example.com/app.tar.gz /app/
Change that single instruction to COPY only if it's copying local files.
If it's a remote URL, the more "correct" fix would normally be using curl with verification, but that would require adding instructions (not allowed).
So in this exam constraint, do NOT pick this unless it's actually a local add like:
ADD . /app
Then change just the word:
COPY . /app
3) Save and exit
:wq
Don't run docker build (task forbids building).
PART B - Fix ONE prominent security/best-practice issue in the Deployment manifest
4) Open the manifest
vi /home/candidate/subtle-bee/deployment.yaml
5) Change ONLY ONE existing field that is a clear security issue
Use / search in vi for the usual "bad fields":
Option 1 (most common): running as root
Search:
/runAsUser
If you see:
runAsUser: 0
Change that one existing field value to:
runAsUser: 65535
✅ This is a single-field change and matches the prompt hint.
Option 2: privileged container
Search:
/privileged
If you see:
privileged: true
Change only that value to:
privileged: false
Option 3: allow privilege escalation
Search:
/allowPrivilegeEscalation
If you see:
allowPrivilegeEscalation: true
Change only that value to:
allowPrivilegeEscalation: false
Option 4: writable root filesystem
Search:
/readOnlyRootFilesystem
If you see:
readOnlyRootFilesystem: false
Change only that value to:
readOnlyRootFilesystem: true
Option 5: image uses :latest
Search:
/image:
If you see:
image: something:latest
Change only that value to a pinned tag, e.g.:
image: something:1.2.3
6) Save and exit
:wq
What to pick (fast decision rule)
If you see run as root in either file, that's usually the highest scoring / most "prominent" security issue.
Dockerfile: USER root → USER 65535
Deployment: runAsUser: 0 → runAsUser: 65535
Those are perfect because you only modify one line/field and it matches the hint.
SIMULATION
Context
You must fully integrate a container image scanner into the kubeadm provisioned cluster.
Task
Given an incomplete configuration located at /etc/kubernetes/bouncer and a functional container image scanner with an HTTPS endpoint at https://smooth-yak.local/review, perform the following tasks to implement a validating admission controller.
First, re-configure the API server to enable all admission plugin(s) to support the provided AdmissionConfiguration.
Next, re-configure the ImagePolicyWebhook configuration to deny images on backend failure.
Next, complete the backend configuration to point to the container image scanner's endpoint at https://smooth-yak.local/review.
Finally, to test the configuration, deploy the test resource defined in /home/candidate/vulnerable.yaml which is using an image that should be denied.
You may delete and re-create the resource as often as needed.
The container image scanner's log file is located at /var/log/nginx/access_log.
Correct Answer:
See the Explanation below for complete solution
Explanation:
Below is the CKS exam style "do-this-exactly" runbook for Q3. It includes the minimal discovery commands (so you don't guess filenames), then the exact lines/blocks to set.
QUESTION 3 - ImagePolicyWebhook (Validating Admission) - Exam Steps
0) SSH + root
ssh cks000002
sudo -i
1) Identify the provided config files (no guessing)
ls -la /etc/kubernetes/bouncer
You are looking for files typically named like:
admission_configuration.yaml (AdmissionConfiguration)
imagepolicywebhook.yaml (ImagePolicyWebhookConfiguration) OR the ImagePolicyWebhook config embedded inside the AdmissionConfiguration kubeconfig (webhook kubeconfig) If unsure which is which, quick peek:
grep -R "ImagePolicyWebhook" -n /etc/kubernetes/bouncer
grep -R "AdmissionConfiguration" -n /etc/kubernetes/bouncer
grep -R "kubeconfig" -n /etc/kubernetes/bouncer
PART A - Reconfigure API Server to enable required admission plugin(s)
2) Edit API server static pod manifest
vi /etc/kubernetes/manifests/kube-apiserver.yaml
2.1 Enable the admission plugin ImagePolicyWebhook
Find the line starting with:
- --enable-admission-plugins=
Ensure ImagePolicyWebhook is included in that comma list.
Example (your list may differ; just add ImagePolicyWebhook):
- --enable-admission-plugins=NodeRestriction,ImagePolicyWebhook
If the flag does not exist, add one line under command::
- --enable-admission-plugins=ImagePolicyWebhook
2.2 Point API server to the provided AdmissionConfiguration
In the same file, ensure this flag exists (use the file in /etc/kubernetes/bouncer that contains AdmissionConfiguration):
- --admission-control-config-file=/etc/kubernetes/bouncer/admission_configuration.yaml If your file is named differently, use the real filename you found in step 1, but keep the flag name exactly --admission-control-config-file.
Save/exit:
:wq
Static pod will restart automatically (kubelet watches the manifest).
Optional quick watch:
docker ps | grep kube-apiserver
# or:
crictl ps | grep kube-apiserver
PART B - Configure ImagePolicyWebhook to deny images on backend failure
3) Edit the ImagePolicyWebhook config
One of these is true on your cluster:
Option 1 (most common in these tasks): ImagePolicyWebhook config is a standalone file Edit the file in /etc/kubernetes/bouncer that contains kind: ImagePolicyWebhookConfiguration:
grep -R "kind: ImagePolicyWebhookConfiguration" -n /etc/kubernetes/bouncer vi /etc/kubernetes/bouncer/<THE_FILE_YOU_FOUND>.yaml Set (or ensure) exactly:
defaultAllow: false
Option 2: ImagePolicyWebhook config is embedded inside AdmissionConfiguration Edit the AdmissionConfiguration file:
vi /etc/kubernetes/bouncer/admission_configuration.yaml
Find the plugin section for ImagePolicyWebhook and ensure the config includes:
defaultAllow: false
✅ Save/exit:
:wq
PART C - Point backend configuration to https://smooth-yak.local/review
4) Edit the webhook kubeconfig to use the scanner endpoint
Find the kubeconfig file referenced by the ImagePolicyWebhook config.
Search for kubeConfigFile:
grep -R "kubeConfigFile" -n /etc/kubernetes/bouncer
Open that kubeconfig path (example name below; yours may differ):
vi /etc/kubernetes/bouncer/kubeconfig
In kubeconfig, set the cluster server exactly:
clusters:
- cluster:
server: https://smooth-yak.local/review
✅ Save/exit:
:wq
PART D - Restart effect (make sure API server picks up config)
Because you already edited /etc/kubernetes/manifests/kube-apiserver.yaml, the API server restarted.
To be safe (and fast), force a restart by "touching" the manifest (no content change needed):
touch /etc/kubernetes/manifests/kube-apiserver.yaml
PART E - Test: apply vulnerable workload and confirm it is denied
5) Use admin kubeconfig (because old kubectl config may break)
export KUBECONFIG=/etc/kubernetes/admin.conf
kubectl get nodes
6) Deploy the test resource (should be DENIED)
kubectl apply -f /home/candidate/vulnerable.yaml
Expected: admission error/denied message.
If it already exists:
kubectl delete -f /home/candidate/vulnerable.yaml
kubectl apply -f /home/candidate/vulnerable.yaml
PART F - Verify the scanner was called (log check)
7) Check scanner access log
tail -n 50 /var/log/nginx/access_log
You should see requests hitting /review.
Quick "what to check if it doesn't deny"
Run these in order:
Confirm API server flags:
grep -n "enable-admission-plugins" /etc/kubernetes/manifests/kube-apiserver.yaml grep -n "admission-control-config-file" /etc/kubernetes/manifests/kube-apiserver.yaml Confirm deny-on-failure:
grep -R "defaultAllow" -n /etc/kubernetes/bouncer
Must show:
defaultAllow: false
Confirm endpoint:
grep -R "server: https://smooth-yak.local/review" -n /etc/kubernetes/bouncer API server logs (docker runtime):
docker ps | grep kube-apiserver
docker logs $(docker ps -q --filter name=kube-apiserver) --tail 80
If you paste the output of:
ls -/etc/kubernetes/bouncer
grep -R "kind: AdmissionConfiguration" -n /etc/kubernetes/bouncer
grep -R "ImagePolicyWebhook" -n /etc/kubernetes/bouncer
SIMULATION
Create a PSP that will prevent the creation of privileged pods in the namespace.
Create a new PodSecurityPolicy named prevent-privileged-policy which prevents the creation of privileged pods.
Create a new ServiceAccount named psp-sa in the namespace default.
Create a new ClusterRole named prevent-role, which uses the newly created Pod Security Policy prevent-privileged-policy.
Create a new ClusterRoleBinding named prevent-role-binding, which binds the created ClusterRole prevent-role to the created SA psp-sa.
Also, Check the Configuration is working or not by trying to Create a Privileged pod, it should get failed.
Correct Answer:
Create a PSP that will prevent the creation of privileged pods in the namespace.
$ cat clusterrole-use-privileged.yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: use-privileged-psp
rules:
- apiGroups: ['policy']
resources: ['podsecuritypolicies']
verbs: ['use']
resourceNames:
- default-psp
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: privileged-role-bind
namespace: psp-test
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: use-privileged-psp
subjects:
- kind: ServiceAccount
name: privileged-sa
$ kubectl -n psp-test apply -f clusterrole-use-privileged.yaml
After a few moments, the privileged Pod should be created.
Create a new PodSecurityPolicy named prevent-privileged-policy which prevents the creation of privileged pods.
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: example
spec:
privileged: false # Don't allow privileged pods!
# The rest fills in some required fields.
seLinux:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
runAsUser:
rule: RunAsAny
fsGroup:
rule: RunAsAny
volumes:
- '*'
And create it with kubectl:
kubectl-admin create -f example-psp.yaml
Now, as the unprivileged user, try to create a simple pod:
kubectl-user create -f- <<EOF
apiVersion: v1
kind: Pod
metadata:
name: pause
spec:
containers:
- name: pause
image: k8s.gcr.io/pause
EOF
The output is similar to this:
Error from server (Forbidden): error when creating "STDIN": pods "pause" is forbidden: unable to validate against any pod security policy: [] Create a new ServiceAccount named psp-sa in the namespace default.
$ cat clusterrole-use-privileged.yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: use-privileged-psp
rules:
- apiGroups: ['policy']
resources: ['podsecuritypolicies']
verbs: ['use']
resourceNames:
- default-psp
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: privileged-role-bind
namespace: psp-test
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: use-privileged-psp
subjects:
- kind: ServiceAccount
name: privileged-sa
$ kubectl -n psp-test apply -f clusterrole-use-privileged.yaml
After a few moments, the privileged Pod should be created.
Create a new ClusterRole named prevent-role, which uses the newly created Pod Security Policy prevent-privileged-policy.
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: example
spec:
privileged: false # Don't allow privileged pods!
# The rest fills in some required fields.
seLinux:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
runAsUser:
rule: RunAsAny
fsGroup:
rule: RunAsAny
volumes:
- '*'
And create it with kubectl:
kubectl-admin create -f example-psp.yaml
Now, as the unprivileged user, try to create a simple pod:
kubectl-user create -f- <<EOF
apiVersion: v1
kind: Pod
metadata:
name: pause
spec:
containers:
- name: pause
image: k8s.gcr.io/pause
EOF
The output is similar to this:
Error from server (Forbidden): error when creating "STDIN": pods "pause" is forbidden: unable to validate against any pod security policy: [] Create a new ClusterRoleBinding named prevent-role-binding, which binds the created ClusterRole prevent-role to the created SA psp-sa.
apiVersion: rbac.authorization.k8s.io/v1
# This role binding allows "jane" to read pods in the "default" namespace.
# You need to already have a Role named "pod-reader" in that namespace.
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
# You can specify more than one "subject"
- kind: User
name: jane # "name" is case sensitive
apiGroup: rbac.authorization.k8s.io
roleRef:
# "roleRef" specifies the binding to a Role / ClusterRole
kind: Role #this must be Role or ClusterRole
name: pod-reader # this must match the name of the Role or ClusterRole you wish to bind to apiGroup: rbac.authorization.k8s.io apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""] # "" indicates the core API group
resources: ["pods"]
verbs: ["get", "watch", "list"]
SIMULATION
A container image scanner is set up on the cluster.
Given an incomplete configuration in the directory
/etc/kubernetes/confcontrol and a functional container image scanner with HTTPS endpoint https://test-server.local.8081/image_policy
1. Enable the admission plugin.
2. Validate the control configuration and change it to implicit deny.
Finally, test the configuration by deploying the pod having the image tag as latest.
Correct Answer:
SeetheExplanationbelowExplanation:
ssh-add ~/.ssh/tempprivate
eval "$(ssh-agent -s)"
cd contrib/terraform/aws
vi terraform.tfvars
terraform init
terraform apply -var-file=credentials.tfvars
ansible-playbook -i ./inventory/hosts ./cluster.yml -e ansible_ssh_user=core -e bootstrap_os=coreos -b --become-user=root --flush-cache -e ansible_user=core