6 exam tips covering domain areas, tricky scenarios, and commands to memorize.
Certified Kubernetes Security Specialist — securing cluster workloads
Explanation
The CKS exam places heavy emphasis on runtime security, and you must be proficient with tools like Trivy and Falco. Trivy is a vulnerability scanner that analyzes container images for known CVEs (Common Vulnerabilities and Exposures) in OS packages and application dependencies. On the exam, you may be asked to scan images, filter results by severity, and decide whether an image is safe to deploy. Falco is a runtime security tool that detects anomalous behavior in running containers by monitoring system calls using eBPF or a kernel module. It uses rules to detect events like unexpected process execution, file access in sensitive directories, or network connections from containers that should not have network access. You need to know how to read Falco rules, understand the output format, and potentially modify rules to detect specific behaviors. Practice running Trivy scans from the command line and interpreting the output table. For Falco, understand the rule structure including the rule name, condition, output, and priority fields. These tools are critical for real-world Kubernetes security and are heavily weighted on the CKS exam.
Command / YAML
# Scan a container image for vulnerabilities with Trivy trivy image nginx:1.25 # Scan and filter only CRITICAL and HIGH severity vulnerabilities trivy image --severity CRITICAL,HIGH nginx:1.25 # Scan an image and output results in JSON format trivy image --format json --output results.json nginx:1.25 # Scan a local image from the Docker daemon trivy image --input /path/to/image.tar # Check Falco service status systemctl status falco # View Falco alerts in real time tail -f /var/log/syslog | grep falco # Example Falco rule to detect shell execution in containers # - rule: Terminal shell in container # desc: Detect a shell being spawned inside a container # condition: spawned_process and container and proc.name in (bash, sh, zsh) # output: "Shell spawned in container (user=%user.name container=%container.name shell=%proc.name)" # priority: WARNING # tags: [container, shell, mitre_execution]
Common Mistake
Candidates only scan images for CRITICAL vulnerabilities and miss HIGH severity issues that the exam question asks about. They also confuse Trivy (image scanning) with Falco (runtime detection) and try to use the wrong tool for the task.
💬 Comments
Explanation
System hardening is a core domain on the CKS exam, and AppArmor and Seccomp profiles are key mechanisms for restricting what containers can do at the kernel level. AppArmor confines programs by limiting their access to files, network, and capabilities based on profiles loaded on the node. To apply an AppArmor profile to a container, you use an annotation on the pod in the format container.apparmor.security.beta.kubernetes.io/<container-name>: localhost/<profile-name>. The profile must already be loaded on every node where the pod might be scheduled. Seccomp (Secure Computing Mode) restricts which system calls a container can make. Since Kubernetes 1.19, you can configure Seccomp using the securityContext.seccompProfile field at both the pod and container level. The RuntimeDefault profile is a good baseline that blocks dangerous syscalls while allowing normal container operations. You should also know how to apply a custom Seccomp profile by placing the JSON profile file in the kubelet's configured seccomp profile directory, typically /var/lib/kubelet/seccomp/. Practice applying both AppArmor and Seccomp profiles and verifying they are active by checking pod security context and node profile status.
Command / YAML
# Pod with AppArmor and Seccomp profiles applied
apiVersion: v1
kind: Pod
metadata:
name: hardened-pod # Name of the security-hardened pod
annotations: # AppArmor is configured via annotations
container.apparmor.security.beta.kubernetes.io/app: localhost/k8s-deny-write # Apply custom AppArmor profile
spec:
securityContext: # Pod-level security context
seccompProfile: # Seccomp profile configuration
type: Localhost # Use a custom local seccomp profile
localhostProfile: profiles/fine-grained.json # Path relative to kubelet seccomp dir
containers:
- name: app # Container name matching the AppArmor annotation
image: nginx:1.25 # Container image
securityContext: # Container-level security settings
allowPrivilegeEscalation: false # Prevent privilege escalation
readOnlyRootFilesystem: true # Make root filesystem read-only
runAsNonRoot: true # Enforce running as non-root user
runAsUser: 1000 # Run as UID 1000
capabilities: # Linux capabilities configuration
drop: # Drop all unnecessary capabilities
- ALL # Remove every capability
# Verify AppArmor profile is loaded on the node
# sudo aa-status | grep k8s-deny-write
# Verify seccomp profile exists at the correct path
# ls /var/lib/kubelet/seccomp/profiles/fine-grained.jsonCommon Mistake
Candidates forget that the AppArmor annotation key must include the exact container name (container.apparmor.security.beta.kubernetes.io/<container-name>), and a typo in the container name silently fails to apply the profile, leaving the container unprotected.
💬 Comments
Explanation
Audit logging allows you to track all requests made to the Kubernetes API server, which is essential for security compliance and incident investigation. On the CKS exam, you may be asked to configure audit logging by creating an audit policy file and updating the kube-apiserver manifest. The audit policy defines rules that determine which events are recorded and at what level. There are four audit levels: None (do not log), Metadata (log request metadata only), Request (log metadata and request body), and RequestResponse (log metadata, request body, and response body). Rules are evaluated in order, and the first matching rule determines the audit level. You configure the API server by adding the --audit-policy-file and --audit-log-path flags to the kube-apiserver static pod manifest. You must also mount the policy file and log directory as volumes into the API server pod. After making changes, the kubelet will automatically restart the API server because it monitors the static pod manifest directory. Practice writing policies that capture sensitive operations like secret access and RBAC changes while avoiding excessive logging of routine health checks and metrics requests.
Command / YAML
# Audit policy file: /etc/kubernetes/audit/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy # Audit policy resource type
rules:
- level: None # Do not log these noisy requests
resources:
- group: "" # Core API group
resources: ["endpoints", "services/status"] # Skip endpoint and status updates
users: ["system:kube-proxy"] # Skip kube-proxy requests
- level: None # Skip health check and metrics endpoints
nonResourceURLs: # Non-resource URL paths
- "/healthz*" # Health check endpoints
- "/livez*" # Liveness endpoints
- "/readyz*" # Readiness endpoints
- level: RequestResponse # Full logging for secrets (sensitive data)
resources:
- group: "" # Core API group
resources: ["secrets"] # Log all secret operations with full detail
- level: Request # Log request body for RBAC changes
resources:
- group: "rbac.authorization.k8s.io" # RBAC API group
resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
- level: Metadata # Log metadata for everything else
resources:
- group: "" # Core API group catch-all
resources: ["*"] # All resources
# Add these flags to kube-apiserver manifest:
# --audit-policy-file=/etc/kubernetes/audit/audit-policy.yaml
# --audit-log-path=/var/log/kubernetes/audit/audit.log
# --audit-log-maxage=30
# --audit-log-maxbackup=10
# --audit-log-maxsize=100Common Mistake
Candidates add the audit flags to the API server manifest but forget to add the corresponding volume and volumeMount entries to make the audit policy file and log directory accessible inside the API server pod, causing the API server to fail to start.
💬 Comments
Explanation
CKS tasks test the principle of least privilege. If a ServiceAccount needs to read ConfigMaps only in the `apps` namespace, you must create a `Role` and a `RoleBinding`, both within that namespace. A `ClusterRole` grants permissions across the entire cluster and is usually incorrect unless the question explicitly asks for it. **Exam Trap**: Using wildcards like `verbs: ['*']` or `resources: ['*']` when the question specifies exact permissions (e.g., 'get' and 'list' on 'pods'). Another trap is incorrect subject formatting for a ServiceAccount in a RoleBinding; it must be `system:serviceaccount:<namespace>:<name>`. Always verify permissions with `kubectl auth can-i` before finishing the task. You can generate the initial objects imperatively to save time.
Command / YAML
# 1. Create the ServiceAccount in the target namespace kubectl create serviceaccount deployer -n apps # 2. Create a Role with specific, namespaced permissions kubectl create role config-reader --namespace=apps \ --verb=get,list,watch --resource=configmaps # 3. Create a RoleBinding to link the ServiceAccount to the Role kubectl create rolebinding deployer-binding --namespace=apps \ --role=config-reader \ --serviceaccount=apps:deployer # 4. Verify the permissions using 'auth can-i' kubectl auth can-i list configmaps --namespace=apps \ --as=system:serviceaccount:apps:deployer # Expected output: yes
Common Mistake
Creating a ClusterRoleBinding to cluster-admin or using wildcard verbs when the task explicitly requires namespace-scoped read-only access.
💬 Comments
Explanation
NetworkPolicy questions on the CKS exam test your understanding of zero-trust networking. The standard pattern is to first apply a 'default deny' policy that blocks all ingress traffic for a namespace, then add specific 'allow' policies. A default deny policy has an empty `podSelector: {}`, which matches all pods in the namespace. **Critical Exam Trap**: The logic of `from` rules. To allow traffic from pods with a specific label *within* a specific namespace, the `podSelector` and `namespaceSelector` must be in the *same* item in the `from` list. If you place them in separate items, it creates an OR condition, allowing traffic from *any* pod with that label or *any* pod in that namespace, which is a security risk and will fail the question.
Command / YAML
# 1. Default-deny all ingress traffic in the 'production' namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {}
policyTypes: ["Ingress"]
# 2. Allow traffic to 'api' pods from 'web' pods in the 'frontend' namespace
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-api
namespace: production
spec:
# This policy applies to pods with the 'app: api' label
podSelector:
matchLabels:
app: api
policyTypes: ["Ingress"]
ingress:
- from:
# This is an AND condition: must match both selectors
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: frontend
podSelector:
matchLabels:
role: web
ports:
- protocol: TCP
port: 8080Common Mistake
Listing podSelector and namespaceSelector as separate ingress from entries (OR logic) when the task requires traffic from pods with specific labels inside a specific namespace (AND logic).
💬 Comments
Explanation
PodSecurityPolicy (PSP) is deprecated. The modern way to enforce security contexts, tested on the CKS, is with Pod Security Admission (PSA) via namespace labels. There are three standard profiles: `privileged` (unrestricted), `baseline` (prevents known escalations), and `restricted` (heavily constrained). You apply these profiles using labels on a namespace for different modes: `enforce` (rejects non-compliant pods), `audit` (logs violations), and `warn` (warns the user). **Exam Trap**: Trying to create a `PodSecurityPolicy` object. The correct method is to label the namespace, e.g., `kubectl label ns my-ns pod-security.kubernetes.io/enforce=restricted`. To comply with the `restricted` profile, a Pod's `securityContext` must explicitly drop all capabilities and run as a non-root user.
Command / YAML
# Apply the 'restricted' profile to a namespace in all modes
kubectl label namespace secure-ns \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted --overwrite
# Example Pod manifest that COMPLIES with the 'restricted' profile
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
namespace: secure-ns
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 1h"]
securityContext:
# Required by 'restricted' profile
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
capabilities:
drop:
- "ALL"Common Mistake
Trying to create PodSecurityPolicy resources or only patching pods when the task requires namespace labels for Pod Security Admission enforce mode.
💬 Comments