Skip to content

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:

watch kubectl get pods -n kube-system
# or
crictl ps | grep apiserver

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:

head -c 32 /dev/urandom | base64

After adding the flag to the API server, existing secrets are not automatically re-encrypted. You must force a rewrite:

kubectl get secrets --all-namespaces -o json | kubectl replace -f -

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
curl -kv https://myapp.example.com --resolve myapp.example.com:443:<ingress-ip>

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-address appropriately, 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 NodePort or LoadBalancer with 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 no in /etc/ssh/sshd_config.
  • Restrict sudo to named users with specific commands rather than blanket ALL=(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:

volumeMounts:
- name: tmp
  mountPath: /tmp
volumes:
- name: tmp
  emptyDir: {}

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 be defaultMode: 0400 and read-only.
  • Set automountServiceAccountToken: false everywhere a pod does not call the API.
  • Restrict get/list on secrets with RBAC. list on 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:

- rule: Contact K8S API Server From Container
  enabled: false

Reload Falco after changes:

systemctl restart falco
# or if running in a pod:
kubectl delete pod -n falco -l app=falco

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

  1. Breaking the API server - editing /etc/kubernetes/manifests/kube-apiserver.yaml with a syntax error. Always cp kube-apiserver.yaml kube-apiserver.yaml.bak before editing. Check the API server came back up after every edit: crictl ps | grep apiserver.

  2. Encryption doesn't apply to existing secrets - after enabling EncryptionConfiguration, run the replacement command to re-encrypt existing secrets.

  3. 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.

  4. Falco rule not taking effect - Falco must be restarted after editing rules. systemctl restart falco or delete the pod.

  5. NetworkPolicy with deny-all egress breaks DNS - always add a port 53 egress allow alongside your deny-all.

  6. Forgetting Pod Security namespace labels - kubectl label namespace syntax is easy to mistype. Verify with kubectl get namespace <name> -o yaml.

  7. Wrong API group in RBAC - core resources (pods, secrets, services) use apiGroups: [""]. Apps resources (deployments) use apiGroups: ["apps"]. Extensions use apiGroups: ["extensions"].


Practice Approach

  1. Killer.sh - mandatory. The CKS simulator is brutal and harder than the real exam. Do it twice and review every missed question.
  2. 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.
  3. Build fluency with kubectl auth can-i --list - you'll use it to verify every RBAC change you make.
  4. Time yourself - the CKS has fewer questions than CKA/CKAD but they're harder. You have less margin to get stuck.
  5. Know where to find things - Falco docs, PSA docs, seccomp/AppArmor examples in the official k8s docs. Navigate fast.

No affiliate relationship: the tools and courses recommended on this page are linked plainly and earn this site nothing.


The Learn pages that map to each CKS domain:

Also: Learning Paths - the track that leads here · CKA Exam Guide (the prerequisite) · Certification Preparation