Certified Kubernetes Security Specialist (CKS)¶
The CKS is the hardest Kubernetes certification. It's hands-on, time-pressured, and tests applied security knowledge across a live cluster - not familiarity with concepts. You need an active CKA before you can sit it, and you'll need that cluster administration depth to solve the problems.
The exam tests your ability to harden what's already there, detect what's happening, and limit blast radius when things go wrong. The questions aren't "configure a cluster from scratch" - they're "fix this insecure thing" or "write this policy to enforce this constraint."
Exam Facts¶
| Format | Browser-based terminal, live cluster |
| Duration | 2 hours |
| Passing score | 67% |
| Price | See current pricing - includes one free retake |
| Prerequisite | Active CKA certification |
| Open book | The shared CKA/CKAD list (kubernetes.io/docs, kubernetes.io/blog and subdomains) plus the CKS-only additions: falco.org/docs, github.com/falcosecurity, aquasecurity.github.io/trivy, app.aquasec.com/static/tracee. The exam's Important Instructions page is the authoritative list |
| Curriculum version | Track github.com/cncf/curriculum for the version tied to your exam date. Exam facts on this page verified 2026-09-09 |
First 3 Minutes: Terminal Setup¶
source <(kubectl completion bash)
alias k=kubectl
complete -F __start_kubectl k
export KUBE_EDITOR=vim
The CKS includes more tool-specific work than CKA/CKAD - falco, trivy, kubesec, apparmor_parser. Know the basic syntax of each before the exam.
Study the Concepts First¶
CKS assumes fluency, not familiarity. Deep-dive companions per domain:
| Exam domain | Weight | Deep-dive companions |
|---|---|---|
| Cluster Setup | 15% | Network Policies · Ingress · Security Primer |
| Cluster Hardening | 15% | RBAC · Security Primer |
| System Hardening | 10% | AppArmor & seccomp · Security Context |
| Minimize Microservice Vulnerabilities | 20% | Pod Security · Security Context · ConfigMaps & Secrets · Cilium |
| Supply Chain Security | 20% | Image Scanning & Signing · Kyverno · OPA & Gatekeeper |
| Monitoring, Logging & Runtime Security | 20% | Falco · Audit & Logging |
Weights are from the CNCF CKS curriculum (github.com/cncf/curriculum) and sum to 100%. Three domains tie for the largest at 20% - Minimize Microservice Vulnerabilities, Supply Chain Security, and Monitoring/Logging/Runtime Security. Together they are 60% of the exam. The sections below run in curriculum order, so section order and study priority line up.
Domain 1: Cluster Setup (15%)¶
Secure API Server Flags¶
The API server is configured via its static pod manifest: /etc/kubernetes/manifests/kube-apiserver.yaml
Commonly tested flags:
# Disable anonymous authentication
- --anonymous-auth=false
# Enable only needed admission plugins
- --enable-admission-plugins=NodeRestriction,PodSecurity
# --insecure-port was removed in Kubernetes 1.24 -- do not add it. The API server
# is HTTPS-only; there is no insecure port left to disable, and passing the flag
# makes the static pod fail to start with an unknown-flag error.
# Audit logging
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
- --audit-log-maxage=30
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
# Encryption at rest
- --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
After editing the static pod manifest, kubelet restarts the API server automatically. Wait for it:
Audit Policies¶
Audit policies control what gets logged. They match events by stage and level.
Stages: RequestReceived, ResponseStarted, ResponseComplete, Panic
Levels:
- None - don't log
- Metadata - log request metadata (who, what resource, when) but not body
- Request - log metadata + request body
- RequestResponse - log metadata + request body + response body
# /etc/kubernetes/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Don't log reads of configmaps and secrets (noisy)
- level: None
resources:
- group: ""
resources: ["configmaps", "secrets"]
verbs: ["get", "list", "watch"]
# Log all changes to pods at RequestResponse level
- level: RequestResponse
resources:
- group: ""
resources: ["pods"]
verbs: ["create", "update", "patch", "delete"]
# Log all actions in sensitive namespaces at Request level
- level: Request
namespaces: ["kube-system", "kube-public"]
# Default: log metadata for everything else
- level: Metadata
Key pattern: rules are evaluated top-to-bottom; first match wins. Put specific rules before the catch-all.
Secrets Encryption at Rest¶
Without this, secrets are stored as base64 (not encrypted) in etcd. The exam often asks you to enable or verify encryption.
# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {} # fallback for existing unencrypted secrets
Generate a key:
After adding the flag to the API server, existing secrets are not automatically re-encrypted. You must force a rewrite:
Verify encryption worked (look for k8s:enc:aescbc prefix in etcd data):
ETCDCTL_API=3 etcdctl get /registry/secrets/default/my-secret \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key | hexdump -C | head
CIS Benchmark Review with kube-bench¶
"Use the CIS benchmark to review the security configuration of Kubernetes components (etcd, kubelet, kubedns, kubeapi)" is a Domain 1 objective, and kube-bench is the tool the exam expects.
# Run everything relevant to this node
kube-bench run
# Target a specific component set
kube-bench run --targets=master,node,etcd,policies
# Run a single check when the question names one
kube-bench run --check=1.2.1
# Machine-readable output
kube-bench run --json | jq '.Controls[].tests[].results[] | select(.status=="FAIL")'
If kube-bench is not installed on the exam node, run it as a job against the host:
kubectl run kube-bench --rm -it --restart=Never \
--image=docker.io/aquasec/kube-bench:latest \
--overrides='{"spec":{"hostPID":true}}' -- node
How to read the output. Each check is [PASS], [FAIL], [WARN] or [INFO], and the == Remediations == block at the end of each section tells you the literal change to make - usually a flag to add to a static pod manifest under /etc/kubernetes/manifests/, or a file permission to tighten:
# Typical remediations you will actually type
chmod 600 /etc/kubernetes/manifests/kube-apiserver.yaml
chown root:root /etc/kubernetes/pki/etcd/server.key
chmod 600 /var/lib/kubelet/config.yaml
WARN items are usually manual checks the tool cannot verify; do not burn exam time on them unless the question asks.
Ingress with TLS¶
The exam asks you to terminate TLS on an Ingress. Two objects: a kubernetes.io/tls secret, and the spec.tls block that references it.
# Self-signed cert, when the question does not supply one
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt -subj "/CN=myapp.example.com"
kubectl create secret tls myapp-tls \
--cert=tls.crt --key=tls.key -n production
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
namespace: production
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true" # force HTTP -> HTTPS
spec:
ingressClassName: nginx
tls:
- hosts:
- myapp.example.com
secretName: myapp-tls # must be in the same namespace as the Ingress
rules:
- host: myapp.example.com # must match a host in spec.tls
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-svc
port:
number: 80
Gotcha: the TLS secret must live in the Ingress's namespace, and the host in spec.tls must match the host in spec.rules exactly or the controller serves its default self-signed certificate instead.
Protect Node Metadata and Endpoints¶
Cloud instance metadata (169.254.169.254) hands out node IAM credentials to anything that can reach it, including a compromised pod. Block it with an egress NetworkPolicy:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-metadata-access
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32
# DNS still needs to work
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Also in scope: keep the kubelet's read-only port off (readOnlyPort: 0 in /var/lib/kubelet/config.yaml), require authentication and authorization on the kubelet API (authentication.anonymous.enabled: false, authorization.mode: Webhook), and keep NodeRestriction in the API server's admission plugins so a compromised kubelet cannot edit other nodes' objects.
# Verify the kubelet is not serving unauthenticated reads
curl -sk https://<node-ip>:10250/pods # expect 401
curl -s http://<node-ip>:10255/pods # expect connection refused
Verify Platform Binaries Before Deploying¶
Before installing a Kubernetes binary, check it against the published checksum:
curl -LO "https://dl.k8s.io/release/v1.31.0/bin/linux/amd64/kubectl"
curl -LO "https://dl.k8s.io/release/v1.31.0/bin/linux/amd64/kubectl.sha256"
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
# kubectl: OK
# Same pattern for any downloaded binary
sha256sum kubeadm kubelet
The same idea applies to container images - verify a signature with cosign verify (see Supply Chain Security) rather than trusting a tag.
NetworkPolicy - CKS-Level Patterns¶
"Use network security policies to restrict cluster level access" is the first objective of this domain. The CKS goes deeper on NetworkPolicy than the CKA. Know deny-all plus specific allow patterns.
Full isolation - deny everything:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Allow egress to DNS only (required with deny-all egress):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Allow backend to receive traffic only from frontend:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-frontend
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- port: 8080
Common mistake: forgetting that deny-all egress blocks DNS. Apps will fail to resolve service names. Always add the DNS allow policy alongside a deny-all egress policy.
Domain 2: Cluster Hardening (15%)¶
Cluster Hardening is 15% - tied with Cluster Setup, and behind the three 20% domains. It covers RBAC and least privilege, exercising caution with service accounts, restricting direct access to the API server, and keeping Kubernetes patched.
Restrict Direct API Access and Dashboard Exposure¶
- Never expose the API server or the Kubernetes Dashboard directly to the internet. Bind
--bind-addressappropriately, and put the API server behind a firewall/security group that only allows known CIDRs. - If the Dashboard is deployed at all, it should sit behind an ingress with authentication, not a
NodePortorLoadBalancerwith no access control. - Keep clusters on a supported, patched Kubernetes minor version - the exam expects you to know why staying current matters (CVE exposure), not just how to run
kubeadm upgrade.
Principle of Least Privilege - The Exam Pattern¶
The exam won't give you overly complex RBAC questions, but it will ask you to: 1. Find an overly permissive role and restrict it 2. Create a minimal role for a specific task 3. Verify permissions work correctly
# Check what a service account can do
kubectl auth can-i --list --as=system:serviceaccount:default:my-sa
# Check a specific permission
kubectl auth can-i delete pods --as=system:serviceaccount:default:my-sa -n default
# Check from outside the cluster (as a user)
kubectl auth can-i get secrets --as=jane -n production
Minimal role - read pods only:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: default
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
What NOT to grant:
# Never this in a real cluster - and CKS will ask you to fix this
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
Bind to a ServiceAccount:
kubectl create rolebinding pod-reader-binding \
--role=pod-reader \
--serviceaccount=default:my-sa \
-n default
Domain 3: System Hardening (10%)¶
The smallest domain, and the only one that is mostly about the host rather than Kubernetes objects: minimize the host OS footprint, use least-privilege identity and access management, minimize external access to the network, and apply kernel hardening tools (AppArmor, seccomp).
Minimize Host OS Footprint¶
# What is actually listening, and does it need to be?
ss -tulpn
systemctl list-units --type=service --state=running
# Remove or mask what you do not need on a Kubernetes node
systemctl stop <service> && systemctl disable --now <service>
systemctl mask <service> # stronger: cannot be started at all
apt purge -y <package>
# Users and shells - no shared logins, no unexpected shells
awk -F: '$3>=1000 && $7!="/usr/sbin/nologin"' /etc/passwd
Prefer a minimal distribution for nodes, keep the kernel patched, and treat any service listening on a node that is not the kubelet, the runtime, or your CNI as something to justify or remove.
Least-Privilege Identity and Access on the Node¶
- Do not give nodes a broad cloud IAM role. A node's instance profile is reachable by every pod that can hit the metadata endpoint, so scope it to exactly what the kubelet and CNI need, and pair it with the metadata NetworkPolicy from Domain 1.
- No shared SSH accounts, key-based auth only,
PermitRootLogin noin/etc/ssh/sshd_config. - Restrict
sudoto named users with specific commands rather than blanketALL=(ALL) NOPASSWD:ALL.
Minimize External Access to the Network¶
# Only the ports Kubernetes actually needs should be open on a control plane node:
# 6443 (API), 2379-2380 (etcd), 10250 (kubelet), 10257/10259 (controller-manager/scheduler)
ufw default deny incoming
ufw allow from <trusted-cidr> to any port 6443 proto tcp
ufw status numbered
# iptables equivalent
iptables -L INPUT -n --line-numbers
Node ports and the etcd client port should never be reachable from outside the cluster's own network.
AppArmor¶
AppArmor profiles restrict what a container process can do at the kernel level.
# Apply AppArmor profile to a container
spec:
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: my-profile # must be loaded on each node
# Check if a profile is loaded on a node
cat /sys/kernel/security/apparmor/profiles | grep my-profile
# Load a profile
apparmor_parser -q /etc/apparmor.d/my-profile
# Check what profile a container is using
kubectl get pod <name> -o yaml | grep apparmor
Seccomp¶
Seccomp filters syscalls available to a container.
securityContext:
seccompProfile:
type: RuntimeDefault # use container runtime's default profile
# or:
type: Localhost
localhostProfile: profiles/my-profile.json # relative to /var/lib/kubelet/seccomp/
Domain 4: Minimize Microservice Vulnerabilities (20%)¶
Pod Security Standards and Admission¶
Pod Security Admission (PSA) replaced PodSecurityPolicy. It enforces security standards at the namespace level via labels.
Three profiles:
- privileged - no restrictions
- baseline - prevents obvious escalation
- restricted - follows all current best practices
Three modes:
- enforce - violating pods are rejected
- audit - violations logged but allowed
- warn - user gets a warning but pod is allowed
# Label a namespace to enforce restricted policy
kubectl label namespace my-ns \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest
# Audit baseline, warn on restricted (common exam pattern)
kubectl label namespace my-ns \
pod-security.kubernetes.io/audit=baseline \
pod-security.kubernetes.io/warn=restricted
What "restricted" requires in the pod spec:
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
securityContext - Know Every Field¶
# Pod-level context
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000 # files created in volumes owned by this group
runAsNonRoot: true # reject if UID=0
sysctls:
- name: net.core.somaxconn
value: "1024"
# Container-level context (overrides pod-level)
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false # can't gain more privs than parent
readOnlyRootFilesystem: true # immutable container FS
privileged: false
runAsNonRoot: true
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"] # only add what's needed
seccompProfile:
type: RuntimeDefault # or Localhost with a custom profile
readOnlyRootFilesystem: true is a high-signal security control. If the app needs to write, use emptyDir volumes for /tmp and other write paths:
Managing Secrets and External Secret Stores¶
"Manage Kubernetes secrets" is an objective here. Know how to read one, how to stop it leaking, and what the alternatives are.
# Read a secret's value
kubectl get secret db-creds -n prod -o jsonpath='{.data.password}' | base64 -d
# Create one without it landing in shell history as plaintext
kubectl create secret generic db-creds --from-file=password=./pw.txt -n prod
# Which pods and service accounts can reach it
kubectl auth can-i get secrets --as=system:serviceaccount:prod:app -n prod
Hardening moves the exam expects:
- Enable encryption at rest (Domain 1) - without it, a secret in etcd is just base64.
- Mount secrets as files, not env vars. Env vars leak into
kubectl describe, crash dumps and child processes; a file mount can bedefaultMode: 0400and read-only. - Set
automountServiceAccountToken: falseeverywhere a pod does not call the API. - Restrict
get/liston secrets with RBAC.liston secrets in a namespace is equivalent to reading all of them.
For anything beyond the cluster's own store, the pattern is an external store (HashiCorp Vault, a cloud KMS/secrets manager) surfaced through the Secrets Store CSI Driver, so the secret is mounted into the pod at runtime and never persisted as a Kubernetes Secret:
spec:
volumes:
- name: secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: vault-db-creds
Pod-to-Pod Encryption¶
The curriculum objective is "implement pod-to-pod encryption" - transparently encrypting traffic between pods rather than trusting the underlay network.
With Cilium, this is a CNI-level setting rather than anything in the pod spec:
# WireGuard transparent encryption
cilium install --set encryption.enabled=true --set encryption.type=wireguard
# Verify it is on and see how many peers are encrypted
cilium status | grep -i encryption
kubectl exec -n kube-system ds/cilium -- cilium-dbg status | grep Encryption
The service-mesh route reaches the same goal with sidecar or ambient mTLS - in Istio, a namespace-scoped PeerAuthentication in STRICT mode:
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # reject any plaintext pod-to-pod traffic in this namespace
Know which one your cluster runs before you start typing: applying mesh policy to a cluster with no mesh installed does nothing at all.
Sandboxed Runtimes (RuntimeClass)¶
RuntimeClass selects a container runtime for a pod. Used to run sensitive workloads in a stronger sandbox (e.g., gVisor, Kata Containers).
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
---
# Use in a pod
spec:
runtimeClassName: gvisor
Open Policy Agent / Gatekeeper¶
OPA/Gatekeeper enforces custom policy via ConstraintTemplate and Constraint objects.
# Check if Gatekeeper is installed
kubectl get pods -n gatekeeper-system
# List constraint templates
kubectl get constrainttemplates
# List constraints
kubectl get constraints
Gatekeeper uses Rego policies. The exam may ask you to create a simple Constraint from an existing ConstraintTemplate:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-team-label
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Namespace"]
parameters:
labels: ["team"]
Admission Controllers¶
# Check which admission controllers are enabled
cat /etc/kubernetes/manifests/kube-apiserver.yaml | grep admission
# Common security-relevant admission controllers:
# - NodeRestriction: limits what kubelet can modify
# - PodSecurity: enforces Pod Security Standards
# - AlwaysPullImages: forces image pull on every pod start (prevents cached image abuse)
Domain 5: Supply Chain Security (20%)¶
Minimize Base Image Footprint¶
Fewer packages means fewer CVEs and no shell for an attacker to land in. This is the first objective of the domain and the cheapest fix in any "reduce this image's vulnerabilities" question.
# Multi-stage: the toolchain never ships
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server
FROM gcr.io/distroless/static-debian12 # no shell, no package manager, no libc extras
COPY --from=build /app /app
USER 65532:65532
ENTRYPOINT ["/app"]
Rough ordering, most to least attack surface: ubuntu/debian > -slim > alpine > distroless > scratch. An exam question that says "make this image more secure" usually wants a smaller base plus a non-root USER.
# Prove it - size and layer history
docker images myapp
docker history myapp:1.0
# Distroless images have no shell, which is the point
docker run --rm -it myapp:1.0 sh # should fail
Also in scope for "understand your supply chain": pin images by digest (image: myapp@sha256:...) rather than a mutable tag, restrict the cluster to permitted registries with an admission policy (see the Gatekeeper and ImagePolicyWebhook sections), and generate an SBOM (trivy image --format cyclonedx -o sbom.json <image>) so you can answer "is this CVE in our fleet?" later.
Image Scanning with Trivy¶
# Scan an image
trivy image nginx:latest
# Scan for HIGH and CRITICAL only
trivy image --severity HIGH,CRITICAL nginx:latest
# Scan without pulling (if already available)
trivy image --skip-update nginx:latest
# Scan a running pod's image
kubectl get pod <name> -o jsonpath='{.spec.containers[*].image}'
trivy image <image>
# Output as JSON (useful for piping)
trivy image -f json -o results.json nginx:latest
What to do with results:
- Identify the CVE IDs and affected packages
- The exam might ask you to find the fixed version or identify which packages are vulnerable
- May ask you to change an image to a more secure alternative (e.g., nginx:alpine)
ImagePolicyWebhook¶
An admission controller that calls an external webhook to approve/deny images.
# /etc/kubernetes/admission-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ImagePolicyWebhook
configuration:
imagePolicy:
kubeConfigFile: /etc/kubernetes/webhook-kubeconfig.yaml
allowTTL: 50
denyTTL: 50
retryBackoff: 500
defaultAllow: false # deny if webhook is unavailable
Add to kube-apiserver:
- --enable-admission-plugins=...,ImagePolicyWebhook
- --admission-control-config-file=/etc/kubernetes/admission-config.yaml
Image Signing with Cosign¶
# Generate a key pair
cosign generate-key-pair
# Sign an image
cosign sign --key cosign.key myregistry.io/myapp:1.0
# Verify signature
cosign verify --key cosign.pub myregistry.io/myapp:1.0
SBOM and kubesec¶
# Static analysis of a Kubernetes manifest
kubesec scan pod.yaml
# Output as JSON
kubesec scan pod.yaml -o json
# kubesec gives a score and specific recommendations
# Score < 0: critical issues, likely to fail if submitted in exam context
Domain 6: Monitoring, Logging & Runtime Security (20%)¶
This domain is one of three that tie for the largest at 20% (with Minimize Microservice Vulnerabilities and Supply Chain Security). Falco is the centerpiece.
Falco - Core Concepts¶
Falco monitors system calls in real time and fires alerts when they match rules. For the CKS: - Know how to modify an existing rule to change its behavior - Know how to write a simple rule - Know where the default rules file is and how to reload Falco
Falco file locations:
/etc/falco/falco.yaml # main config
/etc/falco/falco_rules.yaml # default rules (do not edit directly)
/etc/falco/falco_rules.local.yaml # your custom rules/overrides (edit this)
Falco rule structure:
- rule: Detect Shell in Container
desc: Alert when a shell is spawned in a container
condition: >
spawned_process and
container and
(proc.name = bash or proc.name = sh or proc.name = zsh)
output: >
Shell spawned in container
(user=%user.name container=%container.name image=%container.image.repository
command=%proc.cmdline)
priority: WARNING
tags: [shell, container]
Key Falco fields to know:
proc.name - process name
proc.cmdline - full command line
user.name - user running the process
container.name - container name
container.image.repository - image name
fd.name - file descriptor / filename
evt.type - syscall type (e.g., open, execve, connect)
k8s.pod.name - Kubernetes pod name
k8s.ns.name - Kubernetes namespace
Common exam patterns:
Override a rule to change its output or priority:
# In /etc/falco/falco_rules.local.yaml
- rule: Terminal shell in container
desc: Override - add namespace to output
condition: >
spawned_process and container and
(proc.name = bash or proc.name = sh)
output: >
Shell in container (user=%user.name pod=%k8s.pod.name ns=%k8s.ns.name)
priority: CRITICAL
overwrite: true
Disable a rule:
Reload Falco after changes:
View Falco alerts:
# If running as a service
journalctl -u falco -f
# If running in a pod
kubectl logs -n falco <falco-pod> -f
# Falco writes to syslog by default; also configurable to file
tail -f /var/log/falco.log
Audit Logging - What to Check¶
When an exam question says "review audit logs":
# Audit logs location (set by --audit-log-path)
cat /var/log/kubernetes/audit.log | python3 -m json.tool | less
# Find all kubectl exec events
grep '"verb":"create"' /var/log/kubernetes/audit.log | grep '"subresource":"exec"'
# Find events by user
grep '"username":"attacker"' /var/log/kubernetes/audit.log
# Find secret access
grep '"resource":"secrets"' /var/log/kubernetes/audit.log
High-Value Exam Patterns¶
These come up frequently in CKS scenarios:
Immutable Containers¶
containers:
- name: app
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
Disable Service Account Token Automount¶
# Namespace-wide via ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: my-ns
automountServiceAccountToken: false
# Per-pod override
spec:
automountServiceAccountToken: false
Privilege Escalation Prevention¶
containers:
- securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
runAsNonRoot: true
readOnlyRootFilesystem: true
Verify a Pod Isn't Running as Root¶
kubectl exec -it <pod> -- id
# Should show uid=1000 (not 0)
kubectl exec -it <pod> -- whoami
# Should not return "root"
CKS Common Mistakes¶
-
Breaking the API server - editing
/etc/kubernetes/manifests/kube-apiserver.yamlwith a syntax error. Alwayscp kube-apiserver.yaml kube-apiserver.yaml.bakbefore editing. Check the API server came back up after every edit:crictl ps | grep apiserver. -
Encryption doesn't apply to existing secrets - after enabling
EncryptionConfiguration, run the replacement command to re-encrypt existing secrets. -
Audit policy first-match-wins - if your specific rule comes after the catch-all, it never fires. Always order from most-specific to least-specific.
-
Falco rule not taking effect - Falco must be restarted after editing rules.
systemctl restart falcoor delete the pod. -
NetworkPolicy with deny-all egress breaks DNS - always add a port 53 egress allow alongside your deny-all.
-
Forgetting Pod Security namespace labels -
kubectl label namespacesyntax is easy to mistype. Verify withkubectl get namespace <name> -o yaml. -
Wrong API group in RBAC - core resources (pods, secrets, services) use
apiGroups: [""]. Apps resources (deployments) useapiGroups: ["apps"]. Extensions useapiGroups: ["extensions"].
Practice Approach¶
- Killer.sh - mandatory. The CKS simulator is brutal and harder than the real exam. Do it twice and review every missed question.
- Practice breaking and fixing: enable encryption at rest, audit policy with a specific rule, write a Falco rule that fires, apply PSA labels and see what pods get rejected.
- Build fluency with
kubectl auth can-i --list- you'll use it to verify every RBAC change you make. - Time yourself - the CKS has fewer questions than CKA/CKAD but they're harder. You have less margin to get stuck.
- Know where to find things - Falco docs, PSA docs, seccomp/AppArmor examples in the official k8s docs. Navigate fast.
Recommended Resources¶
No affiliate relationship: the tools and courses recommended on this page are linked plainly and earn this site nothing.
- Killer.sh CKS Simulator - mandatory; included with exam purchase
- K8s Guide mobile app - adaptive CKS practice questions and mastery tracking (iOS now, Android in closed beta). The questions are multiple-choice recall practice: a complement to hands-on cluster time, not a substitute for it, since the exam itself is 100% performance-based.
- KodeKloud CKS Course - comprehensive hands-on labs
- Kubernetes Security Docs - the source of truth for PSA, seccompProfile, securityContext
- Falco Documentation - open book during the exam; bookmark the rules reference
- Official CKS Curriculum - authoritative topic list
- Aqua Security blog - CKS tips - practical walkthroughs of the hardest topics
Related Concepts¶
The Learn pages that map to each CKS domain:
- Cluster Setup (15%) - Network Policies · Ingress · Security Primer
- Cluster Hardening (15%) - RBAC · Kubernetes API
- System Hardening (10%) - AppArmor & seccomp · Kubelet & Runtime
- Minimize Microservice Vulnerabilities (20%) - Pod Security · Security Context · ConfigMaps & Secrets · Cilium
- Supply Chain Security (20%) - Image Scanning & Signing · Kyverno · OPA & Gatekeeper
- Monitoring, Logging & Runtime Security (20%) - Falco · Audit & Logging
Also: Learning Paths - the track that leads here · CKA Exam Guide (the prerequisite) · Certification Preparation