Skip to main content

CKS Cheat Sheet

One-page printable reference for the Certified Kubernetes Security Specialist exam — verified against Kubernetes 1.36. Print it, drill it, then sit the exam.

Last reviewed:

Print tip: Use your browser's "Print" function (Ctrl/Cmd + P). The page is styled to print cleanly on letter or A4 paper. Practice tasks live elsewhere — this is a reference card.

Shell Setup (First Minute of the Exam)

alias k=kubectl
source <(kubectl completion bash)
complete -o default -F __start_kubectl k

# Fast manifest scaffolding
export do='--dry-run=client -o yaml'
export now='--grace-period=0 --force'

# Switch context / namespace quickly
k config use-context <name>
k config set-context --current --namespace=<ns>

Pod Security Admission

Apply per-namespace via labels. Three modes: enforce, audit, warn.

apiVersion: v1
kind: Namespace
metadata:
  name: prod
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

Profiles: privileged (no restrictions), baseline (block known escapes), restricted (heavily hardened — non-root, no caps, etc.).

Read more →

Hardened Pod SecurityContext

apiVersion: v1
kind: Pod
metadata:
  name: hardened
spec:
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot: true
    runAsUser: 65532
    fsGroup: 65532
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: gcr.io/distroless/static:nonroot
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]
      seccompProfile:
        type: RuntimeDefault

Default to dropping every capability and adding back only what the workload strictly needs. NET_BIND_SERVICE for ports below 1024 is the most common exception.

Default-Deny NetworkPolicy

Apply to every namespace before any allow rules.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: prod
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

Allow Ingress From a Namespace

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-frontend
  namespace: prod
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: frontend
    ports:
    - protocol: TCP
      port: 8080

Allow Egress to DNS Only

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: prod
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

More NetworkPolicy patterns →

RBAC

Least-Privilege Role

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: prod
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log"]
  verbs: ["get", "list", "watch"]

Bind to a ServiceAccount

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-pod-reader
  namespace: prod
subjects:
- kind: ServiceAccount
  name: app
  namespace: prod
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

RBAC Audit Commands

# Who can do X?
k auth can-i get secrets --as=system:serviceaccount:prod:app -n prod

# What can a SA do?
k auth can-i --list --as=system:serviceaccount:prod:app -n prod

# Find ClusterRoleBindings using cluster-admin
k get clusterrolebindings -o jsonpath='{range .items[?(@.roleRef.name=="cluster-admin")]}{.metadata.name}{"\n"}{end}'

Common verbs: get list watch create update patch delete deletecollection. Avoid "*" in production roles.

ServiceAccount Token Hardening

apiVersion: v1
kind: ServiceAccount
metadata:
  name: app
  namespace: prod
automountServiceAccountToken: false
---
# Pod-level override when the SA defaults to automount
apiVersion: v1
kind: Pod
metadata:
  name: no-token
spec:
  serviceAccountName: app
  automountServiceAccountToken: false

Bound tokens are projected with audience and TTL. Avoid creating long-lived Secret-based tokens unless absolutely necessary.

Seccomp

Save profile to /var/lib/kubelet/seccomp/profiles/audit.json on every node, then reference by relative path:

apiVersion: v1
kind: Pod
metadata:
  name: seccomp-audit
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/audit.json
  containers:
  - name: app
    image: nginx

Use type: RuntimeDefault for the runtime's default profile. Cluster-wide default-on is enabled with the SeccompDefault feature gate (default since 1.27 when set).

AppArmor (Structured Field, GA in 1.31)

apiVersion: v1
kind: Pod
metadata:
  name: apparmor-pod
spec:
  containers:
  - name: app
    image: nginx
    securityContext:
      appArmorProfile:
        type: Localhost
        localhostProfile: k8s-apparmor-example-deny-write

type values: RuntimeDefault, Localhost, Unconfined. Profiles must be loaded on every node before referencing.

Sandboxed Runtime (gVisor / Kata)

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc
---
apiVersion: v1
kind: Pod
metadata:
  name: untrusted
spec:
  runtimeClassName: gvisor
  containers:
  - name: app
    image: nginx

Image Verification with Kyverno

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-images
spec:
  validationFailureAction: Enforce
  rules:
  - name: verify-cosign
    match:
      any:
      - resources:
          kinds: [Pod]
    verifyImages:
    - imageReferences:
      - "registry.example.com/*"
      attestors:
      - entries:
        - keys:
            publicKeys: |-
              -----BEGIN PUBLIC KEY-----
              <YOUR_KEY>
              -----END PUBLIC KEY-----

Audit Policy

apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- RequestReceived
rules:
# Don't log read-only kubelet probes
- level: None
  users: ["system:kube-proxy"]
  verbs: ["watch"]
# Log auth-related changes at RequestResponse
- level: RequestResponse
  resources:
  - group: ""
    resources: ["secrets", "configmaps", "serviceaccounts"]
- level: RequestResponse
  resources:
  - group: "rbac.authorization.k8s.io"
# Catch-all for all other requests
- level: Metadata

Wire into the API server with --audit-policy-file=/etc/kubernetes/audit/policy.yaml and --audit-log-path=/var/log/kubernetes/audit.log.

kube-bench (CIS)

# Run the relevant benchmark on a control-plane node
kube-bench run --targets master

# Worker node
kube-bench run --targets node

# Pin to a specific benchmark version
kube-bench run --benchmark cis-1.9

# Run as a Job in-cluster
k apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
k logs job/kube-bench

Trivy

# Image scan
trivy image nginx:1.27

# Filesystem / IaC scan
trivy fs --security-checks vuln,config,secret .

# Kubernetes cluster scan
trivy k8s --report summary cluster

# Generate SBOM
trivy image --format spdx-json --output sbom.json nginx:1.27

Cosign — Sign and Verify

# Generate a key pair
cosign generate-key-pair

# Sign an image
cosign sign --key cosign.key registry.example.com/app:1.0

# Verify
cosign verify --key cosign.pub registry.example.com/app:1.0

# Keyless (OIDC) signing — common in CI
cosign sign registry.example.com/app:1.0
cosign verify \
  --certificate-identity 'https://github.com/owner/repo/.github/workflows/ci.yml@refs/heads/main' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  registry.example.com/app:1.0

Falco — Custom Rule

- rule: Shell in container
  desc: Detect a shell spawning inside a container
  condition: >
    container.id != host
    and proc.name in (bash, sh, zsh, ash)
    and proc.tty != 0
  output: >
    Shell spawned in container (user=%user.name container=%container.name
    image=%container.image.repository proc=%proc.cmdline)
  priority: WARNING
  tags: [shell, container]

Read the Falco article →

Encryption at Rest (KMS v2)

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets
  providers:
  - kms:
      apiVersion: v2
      name: kms-provider
      endpoint: unix:///var/run/kmsplugin/socket.sock
      timeout: 3s
  - identity: {}

Pass to the API server via --encryption-provider-config=/etc/kubernetes/enc/enc.yaml. After changes, re-encrypt existing Secrets with k get secrets -A -o json | k replace -f -.

API Server Hardening Flags

FlagRecommended
--anonymous-authfalse (or use --anonymous-auth-config in 1.33+)
--authorization-modeNode,RBAC
--enable-admission-pluginsNodeRestriction,PodSecurity,...
--audit-policy-filePath to a structured policy file
--audit-log-path/var/log/kubernetes/audit.log
--encryption-provider-configKMS v2 config file
--tls-min-versionVersionTLS12 or VersionTLS13
--profilingfalse

kubectl Debug Without Adding a Toolbox

# Debug a running pod with an ephemeral container
k debug -it mypod --image=busybox:1.36 --target=app

# Copy a pod for offline debugging (no privileges granted)
k debug mypod --image=busybox:1.36 --copy-to=mypod-debug --share-processes

# Debug a node
k debug node/<node> -it --image=busybox:1.36

Quick YAML Validation

Use the in-browser YAML Security Analyzer to spot privileged workloads, host mounts, missing capabilities, and other CKS red flags without leaving this site.

Time-Saving kubectl Idioms

# Generate a Pod manifest, edit, apply
k run nginx --image=nginx $do > pod.yaml

# Generate a Deployment
k create deploy web --image=nginx $do > deploy.yaml

# Generate an RBAC Role
k create role pod-reader --verb=get,list --resource=pods $do

# Generate a RoleBinding
k create rolebinding rb --role=pod-reader \
  --serviceaccount=prod:app $do

# Force-delete a stuck pod
k delete pod stuck $now

Cheat sheet good. Now drill it under timer with the practice questions.

Practice QuestionsStudy Plan