CKS — Mock Exam Part 2

16 minute read

Mock Exam Instructions

This mock exam covers CKS Domains 4–6: Minimize Microservice Vulnerabilities, Supply Chain Security, and Monitoring/Logging/Runtime Security.

Rules

  • Time limit: 60 minutes for all 8 questions
  • Open book: Use only kubernetes.io/docs, kubernetes.io/blog, github.com/kubernetes, falco.org/docs, and aquasecurity.github.io/trivy
  • No peeking at answers until you have attempted every question or time runs out
  • Score yourself honestly — partial credit for partially correct answers

Estimated Time per Question

Question Topic Est. Time Points
Q1 PSA: Enforce Restricted Level 3 min 6
Q2 OPA Gatekeeper: Block Untrusted Registries 10 min 14
Q3 Trivy: Scan Image for CRITICAL Vulns 4 min 8
Q4 Encryption at Rest: aescbc Provider 8 min 12
Q5 Falco: Detect Shell in Containers 6 min 10
Q6 Audit Policy: Secrets + Metadata 8 min 14
Q7 Immutable Pod: Full Hardening 5 min 10
Q8 NetworkPolicy: Block Cloud Metadata 5 min 8
Total   49 min 82

Question 1: Enforce PSA Restricted Level on Namespace

Context: kubectl config use-context cluster1-admin@cluster1

Scenario:

The namespace restricted-ns exists. The security team requires that all pods in this namespace comply with the Pod Security Standards Restricted level. Non-compliant pods should be rejected at admission time.

Task:

  1. Label namespace restricted-ns to enforce the restricted Pod Security Standard.
  2. Also add warn and audit labels at the restricted level.
  3. Set the version to latest for all three modes.

Verification:

  • Attempting to create a privileged pod in restricted-ns should be rejected
  • A compliant pod (non-root, drop ALL, readOnlyRootFilesystem) should be accepted

Answer: Question 1

Step 1: Label the namespace

kubectl label namespace restricted-ns \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/warn-version=latest \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/audit-version=latest

Step 2: Verify labels

kubectl get namespace restricted-ns --show-labels

Step 3: Test — non-compliant pod should be rejected

kubectl run test-priv --image=nginx --privileged=true -n restricted-ns
# Should be REJECTED with a message about violating the restricted policy

Step 4: Test — compliant pod should be accepted

# compliant-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: compliant-pod
  namespace: restricted-ns
spec:
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: nginx
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop:
            - ALL
        readOnlyRootFilesystem: true
        runAsNonRoot: true
kubectl apply -f compliant-pod.yaml
# Should be accepted (though the pod may have runtime errors due to nginx needing write access)

Key points:

  • PSA has three modes: enforce (reject), warn (allow but warn), audit (allow and log)
  • The restricted level requires: non-root, no privilege escalation, drop ALL capabilities, Seccomp profile
  • enforce-version=latest uses the latest version of the standards
  • This is a quick 2–3 minute question on the exam — do not overthink it

Question 2: OPA Gatekeeper — Block Untrusted Image Registries

Context: kubectl config use-context cluster1-admin@cluster1

Scenario:

The security team mandates that only images from trusted registries can be deployed in the cluster. Trusted registries are:

  • docker.io/library/
  • gcr.io/mycompany/
  • registry.internal.io/

Any pod using an image from an untrusted registry must be rejected.

Task:

  1. Create an OPA Gatekeeper ConstraintTemplate named k8sallowedregistries that checks whether container images come from an allowed list of registries.
  2. Create a Constraint named allowed-registries that uses this template and specifies the three trusted registries.

Verification:

  • A Pod with image nginx (docker.io/library/nginx) should be accepted
  • A Pod with image malicious-registry.io/backdoor:latest should be rejected

Answer: Question 2

Step 1: Create the ConstraintTemplate

# constraint-template.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedregistries
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRegistries
      validation:
        openAPIV3Schema:
          type: object
          properties:
            registries:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedregistries

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not registry_allowed(container.image)
          msg := sprintf("Image '%v' is not from an allowed registry. Allowed registries: %v", [container.image, input.parameters.registries])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.initContainers[_]
          not registry_allowed(container.image)
          msg := sprintf("Init container image '%v' is not from an allowed registry. Allowed registries: %v", [container.image, input.parameters.registries])
        }

        registry_allowed(image) {
          registry := input.parameters.registries[_]
          startswith(image, registry)
        }
kubectl apply -f constraint-template.yaml

Step 2: Wait for the template to be ready

kubectl get constrainttemplate k8sallowedregistries
# STATUS should show created/ready

Step 3: Create the Constraint

# allowed-registries-constraint.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRegistries
metadata:
  name: allowed-registries
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    registries:
      - "docker.io/library/"
      - "gcr.io/mycompany/"
      - "registry.internal.io/"
kubectl apply -f allowed-registries-constraint.yaml

Step 4: Verify

# Allowed image — should succeed
kubectl run test-allowed --image=docker.io/library/nginx
# OR just:
kubectl run test-allowed --image=nginx
# Note: "nginx" resolves to "docker.io/library/nginx"

# Blocked image — should be rejected
kubectl run test-blocked --image=malicious-registry.io/backdoor:latest
# Should fail with: "Image 'malicious-registry.io/backdoor:latest' is not from an allowed registry"

# Clean up
kubectl delete pod test-allowed --force --grace-period=0

Key points:

  • ConstraintTemplate defines the logic (Rego policy)
  • Constraint defines the parameters (which registries to allow)
  • The Rego code checks both containers and initContainers
  • Short image names like nginx resolve to docker.io/library/nginx — ensure your registry list accounts for this
  • This is a medium-to-hard question (8–12 min) — save it for Pass 2 or 3

Question 3: Trivy — Scan Image for CRITICAL Vulnerabilities

Context: kubectl config use-context cluster1-admin@cluster1

Scenario:

The image nginx:1.21 is being used in production deployments. The security team needs a vulnerability assessment.

Task:

  1. Use Trivy to scan the image nginx:1.21.
  2. List only CRITICAL severity vulnerabilities.
  3. Identify the total number of CRITICAL vulnerabilities found.
  4. If there are CRITICAL vulnerabilities, update the Deployment web-deploy in namespace default to use nginx:1.25 instead (an image with fewer known critical CVEs).

Answer: Question 3

Step 1: Scan the image with Trivy — CRITICAL only

trivy image --severity CRITICAL nginx:1.21

This outputs a table showing only CRITICAL-severity vulnerabilities, including:

  • CVE ID
  • Package name
  • Installed version
  • Fixed version (if available)
  • Description

Step 2: Count CRITICAL vulnerabilities

trivy image --severity CRITICAL nginx:1.21 2>/dev/null | grep "Total:" 
# Or count lines in the table
trivy image --severity CRITICAL --format json nginx:1.21 | \
  python3 -c "import sys,json; data=json.load(sys.stdin); \
  vulns=[v for r in data.get('Results',[]) for v in r.get('Vulnerabilities',[])]; \
  print(f'CRITICAL: {len(vulns)}')"

Step 3: Update the Deployment image

kubectl set image deployment/web-deploy nginx=nginx:1.25 -n default
# OR if the container name is different:
kubectl set image deployment/web-deploy web=nginx:1.25 -n default

Or edit directly:

kubectl edit deployment web-deploy -n default
# Change image: nginx:1.21 to image: nginx:1.25

Step 4: Verify the rollout

# Check deployment status
kubectl rollout status deployment/web-deploy -n default

# Verify the new image
kubectl get deployment web-deploy -n default \
  -o jsonpath='{.spec.template.spec.containers[0].image}'
# Should output: nginx:1.25

# Optionally scan the new image
trivy image --severity CRITICAL nginx:1.25
# Should show fewer CRITICAL vulnerabilities

Key points:

  • --severity CRITICAL filters to only the most serious vulnerabilities
  • Other severity flags: HIGH, MEDIUM, LOW, UNKNOWN
  • You can combine: --severity CRITICAL,HIGH
  • trivy image pulls and scans the image locally
  • On the exam, Trivy is pre-installed — you do not need to install it
  • Always verify the rollout after updating the image

Question 4: Encryption at Rest — Configure aescbc Provider

Context: kubectl config use-context cluster1-admin@cluster1

Scenario:

Secrets in the cluster are currently stored in plaintext in etcd. The security team requires encryption at rest using the aescbc provider.

Task:

  1. Create an EncryptionConfiguration file at /etc/kubernetes/enc/enc.yaml that:
    • Encrypts secrets using the aescbc provider
    • Uses a 32-byte base64-encoded key
    • Falls back to identity (plaintext) for reading existing unencrypted secrets
  2. Configure the API server to use this encryption configuration.

  3. Re-encrypt all existing secrets.

Verification:

  • New secrets should be stored encrypted in etcd
  • Existing secrets should be re-encrypted

Answer: Question 4

Step 1: Generate a 32-byte encryption key

head -c 32 /dev/urandom | base64
# Example output: aGVsbG93b3JsZHRoaXNpc2EzMmJ5dGVz
# Save this value — you need it in the next step

Step 2: Create the EncryptionConfiguration file

mkdir -p /etc/kubernetes/enc
# /etc/kubernetes/enc/enc.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: aGVsbG93b3JsZHRoaXNpc2EzMmJ5dGVz
      - identity: {}
cat << 'EOF' > /etc/kubernetes/enc/enc.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: aGVsbG93b3JsZHRoaXNpc2EzMmJ5dGVz
      - identity: {}
EOF

Important: The identity: {} provider at the end allows the API server to read existing unencrypted secrets. If you omit it, the API server cannot decrypt secrets that were stored before encryption was enabled.

Step 3: Back up the API server manifest

cp /etc/kubernetes/manifests/kube-apiserver.yaml \
   /etc/kubernetes/manifests/kube-apiserver.yaml.bak

Step 4: Modify the API server manifest

vi /etc/kubernetes/manifests/kube-apiserver.yaml

Add the encryption provider flag:

spec:
  containers:
    - command:
        - kube-apiserver
        # ... existing flags ...
        - --encryption-provider-config=/etc/kubernetes/enc/enc.yaml

Add the volume mount:

      volumeMounts:
        # ... existing mounts ...
        - name: enc-config
          mountPath: /etc/kubernetes/enc
          readOnly: true

Add the volume:

  volumes:
    # ... existing volumes ...
    - name: enc-config
      hostPath:
        path: /etc/kubernetes/enc
        type: DirectoryOrCreate

Step 5: Wait for API server to restart

watch crictl ps | grep kube-apiserver
# Wait until the API server is running again (30–60 seconds)

Step 6: Verify encryption is working

# Create a test secret
kubectl create secret generic test-encryption \
  --from-literal=mykey=mydata

# Read the secret directly from etcd
ETCDCTL_API=3 etcdctl get /registry/secrets/default/test-encryption \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt

# The output should be garbled/binary and start with:
# k8s:enc:aescbc:v1:key1:
# This confirms encryption is active

Step 7: Re-encrypt all existing secrets

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

Step 8: Verify an old secret is now encrypted

ETCDCTL_API=3 etcdctl get /registry/secrets/kube-system/default-token-xxxxx \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt

# Should show k8s:enc:aescbc:v1:key1: prefix

Key points:

  • Provider order matters: the first provider is used for writing (encryption), subsequent providers are used for reading (decryption)
  • aescbc first, identity second = encrypt new data, can still read old plaintext data
  • The encryption key must be exactly 32 bytes, base64-encoded
  • Three things needed: EncryptionConfiguration file + API server flag + volume/volumeMount
  • Forgetting to re-encrypt existing secrets is Common Mistake #6

Question 5: Falco — Detect Shell Spawning in Containers

Context: kubectl config use-context cluster2-admin@cluster2

Scenario:

The security team wants to detect when a shell (/bin/bash, /bin/sh, or /bin/dash) is spawned inside any container running in the production namespace.

Task:

  1. Create (or modify) a Falco rule that:
    • Detects shell spawning (bash, sh, or dash) inside containers
    • Only triggers for pods in the production namespace
    • Outputs: timestamp, container name, pod name, namespace, parent process, and shell command
    • Priority: WARNING
  2. Verify the rule by executing a shell in a test pod.

Answer: Question 5

Step 1: Create/edit the Falco rules file

vi /etc/falco/falco_rules.local.yaml

Add the following rule:

- rule: Shell Spawned in Production Container
  desc: Detect shell execution inside containers in the production namespace
  condition: >
    spawned_process and
    container and
    proc.name in (bash, sh, dash) and
    k8s.ns.name = "production"
  output: >
    Shell spawned in production container
    (time=%evt.time container=%container.name pod=%k8s.pod.name
    namespace=%k8s.ns.name parent=%proc.pname command=%proc.cmdline)
  priority: WARNING
  tags: [shell, production, security]

Step 2: Restart Falco to load the new rule

systemctl restart falco

# Verify Falco is running
systemctl status falco

Step 3: Verify the rule is loaded

# Check if the rule appears in Falco's rule list
falco --list 2>&1 | grep "Shell Spawned in Production"

Step 4: Test the rule

# Create a test pod in the production namespace
kubectl run test-shell --image=nginx -n production

# Wait for it to be running
kubectl wait --for=condition=Ready pod/test-shell -n production

# Exec a shell into the pod (this should trigger the Falco rule)
kubectl exec -it test-shell -n production -- /bin/sh -c "echo hello"

Step 5: Check Falco output

# Check Falco logs for the alert
journalctl -u falco | grep "Shell Spawned in Production"
# OR
cat /var/log/syslog | grep "Shell Spawned in Production"

# Should see output like:
# WARNING Shell spawned in production container
# (time=... container=test-shell pod=test-shell
#  namespace=production parent=runc command=sh -c echo hello)

Step 6: Clean up

kubectl delete pod test-shell -n production --force --grace-period=0

Key points:

  • Falco rules go in /etc/falco/falco_rules.local.yaml (local overrides)
  • spawned_process = a new process was executed
  • container = the event occurred inside a container (not on the host)
  • proc.name in (bash, sh, dash) = match common shell binaries
  • k8s.ns.name = "production" = Kubernetes namespace filter
  • Always restart Falco after editing rules
  • The priority field accepts: EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG

Question 6: Audit Policy — Log Secret Access and Metadata

Context: kubectl config use-context cluster1-admin@cluster1

Scenario:

The compliance team requires audit logging with the following policy:

  • All access to Secrets in any namespace must be logged at the Request level (includes the request body)
  • Everything else must be logged at the Metadata level
  • Do NOT log requests to the /healthz endpoint or kube-system lease operations

Task:

  1. Create an audit policy file at /etc/kubernetes/audit/policy.yaml.
  2. Configure the API server to use this audit policy with log backend:
    • Log path: /var/log/kubernetes/audit.log
    • Max age: 30 days
    • Max backups: 10
    • Max size: 100 MB
  3. Verify audit logging is working.

Answer: Question 6

Step 1: Create the audit policy file

mkdir -p /etc/kubernetes/audit
mkdir -p /var/log/kubernetes
# /etc/kubernetes/audit/policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
  - "RequestReceived"
rules:
  # Do not log healthz requests
  - level: None
    nonResourceURLs:
      - "/healthz*"
      - "/livez*"
      - "/readyz*"

  # Do not log kube-system lease operations
  - level: None
    resources:
      - group: "coordination.k8s.io"
        resources: ["leases"]
    namespaces: ["kube-system"]

  # Log all Secret access at Request level
  - level: Request
    resources:
      - group: ""
        resources: ["secrets"]

  # Everything else at Metadata level
  - level: Metadata
cat << 'EOF' > /etc/kubernetes/audit/policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
  - "RequestReceived"
rules:
  - level: None
    nonResourceURLs:
      - "/healthz*"
      - "/livez*"
      - "/readyz*"

  - level: None
    resources:
      - group: "coordination.k8s.io"
        resources: ["leases"]
    namespaces: ["kube-system"]

  - level: Request
    resources:
      - group: ""
        resources: ["secrets"]

  - level: Metadata
EOF

Step 2: Back up the API server manifest

cp /etc/kubernetes/manifests/kube-apiserver.yaml \
   /etc/kubernetes/manifests/kube-apiserver.yaml.bak

Step 3: Modify the API server manifest

vi /etc/kubernetes/manifests/kube-apiserver.yaml

Add the audit flags to the command:

spec:
  containers:
    - command:
        - kube-apiserver
        # ... existing flags ...
        - --audit-policy-file=/etc/kubernetes/audit/policy.yaml
        - --audit-log-path=/var/log/kubernetes/audit.log
        - --audit-log-maxage=30
        - --audit-log-maxbackup=10
        - --audit-log-maxsize=100

Add volume mounts:

      volumeMounts:
        # ... existing mounts ...
        - name: audit-policy
          mountPath: /etc/kubernetes/audit
          readOnly: true
        - name: audit-log
          mountPath: /var/log/kubernetes
          readOnly: false

Add volumes:

  volumes:
    # ... existing volumes ...
    - name: audit-policy
      hostPath:
        path: /etc/kubernetes/audit
        type: DirectoryOrCreate
    - name: audit-log
      hostPath:
        path: /var/log/kubernetes
        type: DirectoryOrCreate

Step 4: Wait for API server to restart

watch crictl ps | grep kube-apiserver
# Wait until the API server is running (30–60 seconds)

Step 5: Verify audit logging

# Generate some audit events
kubectl get secrets -n default
kubectl get pods -n default

# Check the audit log
tail -20 /var/log/kubernetes/audit.log

# Check that Secret access is logged at Request level
cat /var/log/kubernetes/audit.log | grep '"resource":"secrets"' | \
  python3 -c "import sys,json; [print(json.loads(l).get('level')) for l in sys.stdin]"
# Should show "Request"

# Check that other resources are logged at Metadata level
cat /var/log/kubernetes/audit.log | grep '"resource":"pods"' | \
  python3 -c "import sys,json; [print(json.loads(l).get('level')) for l in sys.stdin]"
# Should show "Metadata"

Key points:

  • Audit rules are evaluated in order — first match wins
  • level: None rules must come first to exclude noisy/unnecessary events
  • Four audit levels: None < Metadata < Request < RequestResponse
  • Request level includes the request body (critical for Secret auditing)
  • Three things needed in API server: flags + volumeMounts + volumes (Common Mistake #11)
  • omitStages: ["RequestReceived"] avoids duplicate logging

Question 7: Create a Fully Hardened Immutable Pod

Context: kubectl config use-context cluster1-admin@cluster1

Scenario:

Create a maximally hardened pod that follows all CKS security best practices.

Task:

Create a Pod named hardened-pod in namespace default with these specifications:

  • Image: nginx:1.25
  • Container name: app
  • Security requirements:
    • Read-only root filesystem
    • Drop ALL capabilities
    • Do not allow privilege escalation
    • Run as non-root user (UID 1000)
    • Run as non-root group (GID 1000)
    • Seccomp profile: RuntimeDefault
  • The pod should not mount the ServiceAccount token

Answer: Question 7

Step 1: Create the Pod YAML

# hardened-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hardened-pod
  namespace: default
spec:
  automountServiceAccountToken: false
  securityContext:
    runAsUser: 1000
    runAsGroup: 1000
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: nginx:1.25
      securityContext:
        readOnlyRootFilesystem: true
        allowPrivilegeEscalation: false
        capabilities:
          drop:
            - ALL
      volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: nginx-cache
          mountPath: /var/cache/nginx
        - name: nginx-run
          mountPath: /var/run
  volumes:
    - name: tmp
      emptyDir: {}
    - name: nginx-cache
      emptyDir: {}
    - name: nginx-run
      emptyDir: {}
kubectl apply -f hardened-pod.yaml

Note: nginx needs write access to /tmp, /var/cache/nginx, and /var/run. Since readOnlyRootFilesystem: true prevents writing to these paths, we mount emptyDir volumes to provide writable temporary storage.

Step 2: Verify the pod is running

kubectl get pod hardened-pod
# May take a moment; if it fails, check events:
kubectl describe pod hardened-pod

Step 3: Verify security settings

# Check security context
kubectl get pod hardened-pod -o yaml | grep -A 15 securityContext

# Verify readOnlyRootFilesystem
kubectl exec hardened-pod -- touch /test-file 2>&1
# Should fail: "Read-only file system"

# Verify writable tmp works
kubectl exec hardened-pod -- touch /tmp/test-file
# Should succeed

# Verify running as non-root
kubectl exec hardened-pod -- id
# Should show: uid=1000 gid=1000

# Verify no SA token mounted
kubectl exec hardened-pod -- ls /var/run/secrets/kubernetes.io/serviceaccount/ 2>&1
# Should show: No such file or directory

# Verify capabilities dropped
kubectl exec hardened-pod -- cat /proc/1/status | grep -i cap
# CapBnd should show 0000000000000000 (all dropped)

Key points:

  • Pod-level securityContext: runAsUser, runAsGroup, runAsNonRoot, seccompProfile
  • Container-level securityContext: readOnlyRootFilesystem, allowPrivilegeEscalation, capabilities
  • automountServiceAccountToken: false at the pod level prevents token mounting
  • nginx requires writable directories — use emptyDir volumes for these
  • This pod meets all requirements of the PSS Restricted profile
  • On the CKS exam, you may not need the emptyDir volumes if the question only asks for the security fields (the pod will crash but the security specs are correct)

Question 8: Block Egress to Cloud Metadata Endpoint

Context: kubectl config use-context cluster2-admin@cluster2

Scenario:

Pods in the app namespace must be prevented from accessing the cloud provider metadata service at 169.254.169.254. This is critical because the metadata endpoint can expose IAM credentials and other sensitive information.

Task:

  1. Create a NetworkPolicy named deny-metadata-egress in namespace app that:
    • Applies to all pods in the namespace
    • Blocks egress traffic to 169.254.169.254/32 on all ports
    • Allows all other egress traffic

Answer: Question 8

Step 1: Create the NetworkPolicy

To block traffic to a specific IP while allowing everything else, use an egress rule with an ipBlock that covers all IPs except the metadata endpoint:

# deny-metadata-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-metadata-egress
  namespace: app
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    # Allow all egress EXCEPT to 169.254.169.254
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
              - 169.254.169.254/32
kubectl apply -f deny-metadata-egress.yaml

Step 2: Verify

# Test that the metadata endpoint is blocked
kubectl exec -n app <pod-name> -- wget -qO- --timeout=2 http://169.254.169.254/latest/meta-data/
# Should timeout or be blocked

# Test that other egress works
kubectl exec -n app <pod-name> -- wget -qO- --timeout=2 http://nginx.default.svc
# Should succeed (if there is an nginx service in default namespace)

# Test DNS still works
kubectl exec -n app <pod-name> -- nslookup kubernetes
# Should resolve

Key points:

  • cidr: 0.0.0.0/0 matches ALL IPv4 addresses
  • except: ["169.254.169.254/32"] carves out the metadata endpoint
  • This approach allows all egress EXCEPT to the blocked IP
  • The metadata endpoint 169.254.169.254 is used by AWS (IMDS), GCP, and Azure for instance metadata
  • Blocking this endpoint prevents pods from stealing cloud IAM credentials
  • Note: this policy does not block cluster-internal traffic (pod-to-pod) because that uses cluster IPs, not the metadata IP
  • If you also need to block IPv6 metadata, add fe80::/10 exclusions separately

Scoring Guide

Question Max Points Your Score
Q1: PSA Restricted 6 ___
Q2: OPA Gatekeeper 14 ___
Q3: Trivy Scan 8 ___
Q4: Encryption at Rest 12 ___
Q5: Falco Rule 10 ___
Q6: Audit Policy 14 ___
Q7: Immutable Pod 10 ___
Q8: Metadata Block 8 ___
Total 82 ___

How to Interpret Your Score

Score Readiness
70–82 Excellent — Domains 4–6 are solid
55–69 Good — Review the questions you missed
35–54 Needs work — Revisit the relevant chapters
Below 35 Not ready — Go through Domains 4–6 tutorial material again

Combined Score (Part 1 + Part 2)

Combined Score CKS Readiness
120–152 Ready to take the exam — schedule it this week
100–119 Almost ready — do one more review cycle
80–99 Needs practice — take the mock exams again in 3–5 days
Below 80 Significant gaps — review the full tutorial before retaking

Updated: