CKS: Certified Kubernetes Security Specialist Study Guide
The Certified Kubernetes Security Specialist (CKS) is a hands-on, performance-based exam that validates your ability to secure container-based applications and Kubernetes platforms across build, deployment, and runtime. It assumes you already hold a valid CKA and is aimed at platform engineers, SREs, and security practitioners who harden clusters, lock down workloads, secure the supply chain, and monitor for threats. You get 120 minutes to solve live tasks on real clusters and must score at least 67%.
Domain 1: Cluster Setup
- kube-bench checks a cluster against the CIS Kubernetes Benchmark; run 'kube-bench run --targets master' on a control-plane node and '--targets node' on workers to scope the checks.
- An empty podSelector ({}) selects all pods in a namespace; a policy with policyTypes:[Ingress] and no ingress rules denies all inbound, and one with policyTypes:[Egress] and no egress rules denies all outbound, forming a default-deny baseline.
- NetworkPolicies are only enforced if the CNI supports them (Calico, Cilium, Weave); plain flannel ignores them, so applying a policy with an unsupported CNI silently allows all traffic.
- To block pod access to the cloud metadata endpoint, write an egress policy whose allowed destinations exclude 169.254.169.254/32, e.g. ipBlock cidr 0.0.0.0/0 with except [169.254.169.254/32].
- On kubeadm clusters the kube-apiserver runs as a static pod in /etc/kubernetes/manifests/; editing that manifest makes the kubelet automatically recreate the pod, so no restart command is needed.
- Encryption at rest is configured by an EncryptionConfiguration manifest (providers like aescbc, secretbox, or kms) referenced via --encryption-provider-config; after enabling it you must re-write existing Secrets (kubectl get secrets -A -o json | kubectl replace -f -) to encrypt them.
- Harden the kubelet by setting --read-only-port=0 (or readOnlyPort:0), --anonymous-auth=false, and --authorization-mode=Webhook to close unauthenticated access to /exec, /logs, and metrics.
- Recommended apiserver authorization is --authorization-mode=Node,RBAC; the Node authorizer restricts what each kubelet can read/write, and NodeRestriction admission stops a kubelet from editing other nodes' objects.
- Disable unneeded apiserver surface with --profiling=false and --anonymous-auth=false, and bind components to loopback with --bind-address=127.0.0.1 on the controller-manager and scheduler manifests.
- etcd data must be protected: the data directory should be owned by etcd:etcd with mode 700, with client TLS (--client-cert-auth, --trusted-ca-file) and peer TLS (--peer-client-cert-auth) enabled.
- PodSecurity admission is enabled by default in recent Kubernetes versions and enforces the Pod Security Standards via namespace labels rather than the removed PodSecurityPolicy.
- Enforce TLS by setting --tls-min-version=VersionTLS12 (or TLS13) and a strong --tls-cipher-suites list on the apiserver and kubelet to reject weak ciphers.
- --enable-admission-plugins is additive to the default set; use --disable-admission-plugins only to turn off a specific default you do not want, and never assume it replaces the defaults.
- Verify downloaded Kubernetes binaries by downloading the official sha256 checksum and comparing it against the binary before installing, to detect tampering.
- Anonymous API calls map to user system:anonymous and group system:unauthenticated; even if anonymous auth is left on, RBAC grants them no meaningful permissions so authorization denies the request.
Domain 2: Cluster Hardening
- Set automountServiceAccountToken:false on a pod or ServiceAccount when the workload does not call the API, removing the token at /var/run/secrets/kubernetes.io/serviceaccount and shrinking attack surface.
- Patch the default SA to stop mounting tokens with: kubectl patch serviceaccount default -p '{"automountServiceAccountToken":false}'.
- Use Roles (namespace-scoped) not ClusterRoles for least privilege: 'kubectl create role pod-reader --verb=get,list --resource=pods -n web' then 'kubectl create rolebinding ... --role=pod-reader --user=dev -n web'.
- Test effective permissions with 'kubectl auth can-i', impersonating with --as; a ServiceAccount is referenced as system:serviceaccount:<namespace>:<name>, and 'kubectl auth can-i --list --as=...' enumerates everything a subject can do.
- RBAC enforces privilege-escalation prevention: you can only create or update a role with permissions you already hold, unless you have the special 'escalate' verb; binding a role you don't hold requires the 'bind' verb, so granting 'bind' on a powerful role enables escalation.
- The system:masters group and binding the cluster-admin ClusterRole to broad subjects grants full cluster control; remove such bindings and grant scoped least-privilege roles instead.
- Upgrade a kubeadm control plane with 'kubeadm upgrade apply v1.30.2' on the first control-plane node, then 'kubeadm upgrade node' on the others; renew all PKI with 'kubeadm certs renew all'.
- Prefer short-lived projected ServiceAccount tokens (serviceAccountToken volume projection with expirationSeconds and audience) over the legacy long-lived Secret-based tokens.
- Enable kubelet certificate rotation with --rotate-certificates=true and --rotate-server-certificates=true so client and serving certs renew automatically before expiry.
- Approve node and user CertificateSigningRequests with 'kubectl certificate approve <csr-name>'; review and deny unexpected CSRs to prevent rogue clients from obtaining credentials.
- Cordon and evict workloads before maintenance with 'kubectl drain <node> --ignore-daemonsets' (drain also cordons), and 'kubectl uncordon' to return it to service.
- Audit RBAC with 'kubectl get clusterrolebindings -o wide' to review subjects and roles, looking especially for bindings to default ServiceAccounts or wide groups.
- Aggregated ClusterRoles use an aggregationRule with a clusterRoleSelector matching a label; the controller auto-merges rules from labeled ClusterRoles, so adding a labeled role expands the aggregate automatically.
- Restrict the kubelet API by combining --anonymous-auth=false (callers without credentials get 401) with --authorization-mode=Webhook so the apiserver authorizes each kubelet request.
- Avoid ABAC: use Node,RBAC and remove ABAC because ABAC policies are file-based, require an apiserver restart to change, and are hard to audit.
Domain 3: System Hardening
- In Kubernetes 1.30+, AppArmor is set via securityContext.appArmorProfile with type:Localhost and localhostProfile:<name> for a node-loaded profile; the older container.apparmor.security.beta.kubernetes.io annotation is deprecated.
- Set seccompProfile.type:RuntimeDefault to apply the container runtime's built-in syscall filter; type:Localhost with localhostProfile points to a custom profile, and type:Unconfined disables filtering.
- Custom localhost seccomp profiles live under the kubelet seccomp root (default /var/lib/kubelet/seccomp), so a file at /var/lib/kubelet/seccomp/profiles/myprofile.json is referenced as localhostProfile:profiles/myprofile.json.
- A seccomp profile uses defaultAction:SCMP_ACT_ERRNO to deny by default, then an allowlist of syscalls with action SCMP_ACT_ALLOW; this is the default-deny pattern the exam expects.
- Use a RuntimeClass with handler:runsc and set spec.runtimeClassName on a pod to run it under gVisor (sandboxed runtime) for stronger isolation of untrusted or multi-tenant workloads.
- Drop all Linux capabilities and add back only what is required, e.g. capabilities:{drop:[ALL], add:[NET_BIND_SERVICE]}; NET_BIND_SERVICE is the single cap needed to bind ports below 1024.
- Drop NET_RAW (or ALL) and set allowPrivilegeEscalation:false to block raw-socket spoofing and prevent a process from gaining more privileges than its parent.
- A strong restrictive securityContext combines runAsNonRoot:true, allowPrivilegeEscalation:false, readOnlyRootFilesystem:true, capabilities.drop:[ALL], and seccompProfile.type:RuntimeDefault.
- Minimize host attack surface by removing or disabling unused packages, services, daemons, and open ports, and by blacklisting unneeded kernel modules (modprobe blacklist) on a minimal, patched OS image.
- Set kubelet --protect-kernel-defaults=true so the kubelet refuses to start if node sysctl kernel settings differ from its safe defaults, preventing silent insecure overrides.
- Kubernetes disables swap by default for nodes because swap undermines the kubelet's memory accounting and limit enforcement, breaking predictable memory isolation.
- Unsafe sysctls are blocked unless explicitly allowed via the kubelet --allowed-unsafe-sysctls list; only add a sysctl there when you accept the security tradeoff.
- User namespaces (spec.hostUsers:false) map container root to an unprivileged host UID, so a container escape does not yield real root on the node.
- Load AppArmor profiles onto every node before pods reference them, typically via a privileged DaemonSet or node bootstrap that runs apparmor_parser / aa-enforce in enforce mode.
- procMount defaults to 'Default', which masks sensitive /proc paths; setting 'Unmasked' removes that masking and is discouraged because it exposes host process information.
Domain 4: Minimize Microservice Vulnerabilities
- Enforce the restricted Pod Security Standard on a namespace with the label: kubectl label ns prod pod-security.kubernetes.io/enforce=restricted; the modes are enforce, audit, and warn.
- Pod Security Admission is configured purely by namespace labels of the form pod-security.kubernetes.io/<mode>=<level> where level is privileged, baseline, or restricted; baseline disallows hostPath and privileged, restricted additionally requires runAsNonRoot and a non-Unconfined seccomp profile.
- Prefer mounting secrets as files over environment variables, because env vars can leak via child processes, crash dumps, and 'kubectl describe', whereas file mounts can be permissioned and are less exposed.
- Mount Secret volumes readOnly:true with a restrictive defaultMode such as 0400, and enable encryption at rest so the secret is protected in etcd and on disk.
- Use a service mesh (Istio, Linkerd) with sidecar proxies to enforce mTLS between pods without changing application code; in Istio a PeerAuthentication with mtls.mode:STRICT in the namespace requires mutual TLS.
- OPA Gatekeeper uses a two-object pattern: a ConstraintTemplate defines the Rego policy and generates a CRD, and a Constraint instance scopes and enforces it against matching resources.
- Kyverno enforces image rules with a validate rule (set validationFailureAction/Enforce) that fails when an image ends with ':latest' or has no tag, blocking mutable tags at admission.
- Build a least-privilege NetworkPolicy by applying a default-deny ingress (and egress) policy per namespace, then adding allow rules; remember to allow egress to CoreDNS/kube-dns on UDP and TCP port 53 or name resolution breaks.
- Restrict cross-namespace traffic with a namespaceSelector in NetworkPolicy ingress rules so only explicitly chosen namespaces can reach a workload, defaulting everything else to deny.
- Reduce blast radius after compromise with allowPrivilegeEscalation:false and readOnlyRootFilesystem:true (using writable emptyDir volumes for paths that need writes).
- Issue and distribute workload certificates via a service mesh or cert-manager that writes certs into Secrets the workloads consume, avoiding hand-managed key material.
- Run untrusted tenant pods under a sandboxed runtime by defining a RuntimeClass (handler:runsc for gVisor) and setting runtimeClassName only on those tenant pods, leaving trusted workloads on the default runtime.
- Enforce runAsNonRoot at the namespace level with Pod Security Admission 'restricted', which rejects any pod that would run as UID 0.
- Combine a default-deny egress NetworkPolicy with an egress rule allowing only the specific backend pods and port to constrain lateral movement from a compromised microservice.
- Block access to the cloud metadata service from pods with a NetworkPolicy egress ipBlock of cidr 0.0.0.0/0 and except [169.254.169.254/32].
Domain 5: Supply Chain Security
- Scan a container image and fail CI on findings with: trivy image --severity HIGH,CRITICAL --exit-code 1 myreg/app:1.0; trivy fs --severity HIGH,CRITICAL . scans a filesystem or source tree.
- Pin images by immutable digest (myreg/app@sha256:<digest>) so a repointed tag cannot silently swap in malicious content; combine with minimal/distroless or scratch base images and multi-stage builds.
- Restrict pods to approved registries with an admission policy (OPA Gatekeeper, Kyverno, or the built-in ImagePolicyWebhook) that rejects image references not starting with the allowed registry prefix, e.g. requiring registry.internal/ and denying docker.io/*.
- The built-in ImagePolicyWebhook admission plugin is configured via --admission-control-config-file and calls an external service to allow or deny images at admission time.
- cosign signs images for provenance: 'cosign sign --key cosign.key myreg/app@sha256:<digest>' to sign and 'cosign verify --key cosign.pub' to verify; enforce verification at admission with a Kyverno verifyImages rule or the sigstore policy-controller.
- Keyless signing uses 'cosign sign' with an OIDC identity backed by Fulcio (short-lived certs) and Rekor (transparency log), avoiding long-lived signing keys.
- Attach signed attestations such as an SBOM with 'cosign attest' (a signed predicate), separating the artifact's provenance metadata from its signature.
- Generate an SBOM enumerating an image's components and dependencies so they can be matched against CVE databases; scan an SBOM with grype, e.g. 'grype sbom:./sbom.json'.
- Use static manifest analyzers kubesec and kube-score to flag insecure settings (privileged, hostPath, missing limits or securityContext) before manifests are applied.
- Lint Dockerfiles with hadolint to catch missing USER (running as root), use of the latest tag, and other best-practice violations during the build stage.
- Harden Dockerfiles by adding a non-root USER, dropping unnecessary tooling, and using a distroless or scratch base image to shrink the runtime attack surface.
- Setting imagePullPolicy:Always forces a registry pull and authorization on every pod start, preventing a node's cached image from being reused by a pod whose user lacks pull rights.
- Trivy Operator continuously scans cluster workloads and publishes results as VulnerabilityReport custom resources you can query with kubectl.
- A complete image-trust admission policy needs two rules: one verifying the cosign signature and one requiring the image reference to match the approved registry prefix, both with Enforce action.
- Digest pinning guarantees the exact reviewed base layers are deployed, which is why supply-chain hardening pairs digest references with scanning and signature verification rather than relying on tags.
Domain 6: Monitoring, Logging and Runtime Security
- Audit logging requires both a policy and a backend: set --audit-policy-file=/etc/kubernetes/audit/policy.yaml and --audit-log-path=/var/log/kubernetes/audit.log on the apiserver static pod; without the policy file no events are recorded.
- Kubernetes audit levels are None, Metadata, Request, and RequestResponse; RequestResponse is the most verbose, capturing event metadata plus the full request and response bodies.
- Audit Policy rules are evaluated top-down and the first matching rule wins, so put a specific rule (e.g. level:RequestResponse for resources:secrets) before any catch-all Metadata rule.
- Scope an audit rule to Secrets in one namespace with resources:[{group:'', resources:['secrets']}] and namespaces:['prod'] to capture caller, verb, and timestamp for those access events.
- Set omitStages:['RequestReceived'] at the top of the Policy to skip the noisy initial stage and only log events at later stages.
- Control audit log rotation and retention with --audit-log-maxage (days to keep), --audit-log-maxbackup (number of files), and --audit-log-maxsize (MB per file).
- Send audit events to a remote sink with --audit-webhook-config-file=/etc/kubernetes/audit/webhook.yaml alongside the audit policy file.
- Falco is a CNCF runtime security tool that taps the kernel via a loadable kernel module or a modern eBPF probe to observe syscalls and detect anomalous behavior in real time.
- Falco rules live in YAML files referenced under rules_files in falco.yaml; each rule has a condition built from filter fields, plus reusable lists (named value collections) and macros (named reusable conditions).
- A Falco rule condition combines an event type and field match, e.g. detecting writes under /etc with (evt.type in (open,openat) and fd.name startswith /etc), or interactive shells in a container to flag possible compromise.
- Each Falco rule sets a priority (severity) that classifies the alert and can filter what is forwarded; outputs go to stdout, syslog, files, or external sinks per falco.yaml.
- Add custom Falco rules in a separate file such as falco_rules.local.yaml listed after the defaults under rules_files, so your rules can override the shipped ones.
- Falco detects container drift and unexpected exec by comparing running binaries against the image's read-only layers and flagging executables that did not come from the image.
- Pair readOnlyRootFilesystem:true (limits writes) with Falco drift and exec detection (alerts on unexpected binaries and shells) for layered immutable-infrastructure protection.
- Collect container logs at the node level with a DaemonSet such as Fluent Bit or Fluentd that reads container log files and forwards them to a central backend, since deleted pods lose local logs.
CKS exam tips
- Set up a kubectl alias and context-switching habit early; the exam spans multiple clusters and you must run 'kubectl config use-context <name>' before each task or you will edit the wrong cluster.
- Bookmark the allowed docs (kubernetes.io, Falco, Trivy, gVisor, Kyverno, AppArmor) and practice navigating them fast; you cannot memorize every YAML field but you can find it quickly.
- On kubeadm clusters, edit static pod manifests in /etc/kubernetes/manifests/ and wait for the kubelet to recreate the apiserver pod; verify with 'kubectl get pod -n kube-system' or 'crictl ps' instead of trying to restart it manually.
- Always read the task's target namespace and cluster carefully, and after applying NetworkPolicies remember to allow DNS egress on port 53 or you will break the workload you were asked to secure.
- Manage your 120 minutes by weight: flag and skip a stubborn task, finish the higher-count domains (Cluster Hardening, Microservice Vulnerabilities, System Hardening) first, and return to skipped items at the end.
Study guide FAQ
Do I need to pass the CKA before taking the CKS?
Yes. CKS requires a current, valid CKA (Certified Kubernetes Administrator) certification as a prerequisite. You must hold it at the time you schedule and sit the CKS exam, and it must not have expired.
Is the CKS exam multiple choice or hands-on?
It is entirely performance-based. You solve real tasks on live Kubernetes clusters from a terminal within 120 minutes. There are no multiple-choice questions; you must actually configure RBAC, write NetworkPolicies, run scanners, edit manifests, and harden nodes.
What is the passing score and how long is the exam?
You have 120 minutes and need a score of at least 67% to pass. The exam is proctored online, and you are allowed one open browser tab to the official Kubernetes documentation and a short list of permitted project sites (such as Falco, Trivy, and gVisor docs).
Which third-party tools should I be comfortable with for the CKS?
Focus on the tools the curriculum references: kube-bench (CIS benchmark), Trivy and grype (image and SBOM scanning), kubesec/kube-score/hadolint (static analysis), cosign/sigstore (image signing), OPA Gatekeeper and Kyverno (admission policy), AppArmor and seccomp (sandboxing), gVisor (RuntimeClass), and Falco (runtime detection). Practice installing and running each one by hand.