TT Lab
Get started
Learn Learning paths Courses

KCSA — Kubernetes Security Associate

Open the Cluster With an Attacker's Eyes

Continue in TT Lab

This lab runs on a real cluster

KCSA is about threat models and attack surface. To learn it, you have to actually steal something — pull plaintext out of the store, read a Secret even though you have no permission, and see the traces it leaves in the audit log.

A fake cluster has no store, no apiserver authentication chain, and no audit log, so all of this existed only as text. Hearing that base64 is not encryption is different from pulling plaintext out of the store yourself.

This lab is kept from overlapping with CKS. CKS is a lab about blocking, and here in KCSA we show why you must block through attacks.

It takes about 2 minutes to come up the first time.

Steps

  1. Create a secret and pull it out of the store in plaintext, and put it in /root/kcsa/plaintext.txt.
  2. Turn on encryption at rest and put the fact that the plaintext disappears in /root/kcsa/encrypt.txt.
  3. Put the fact that old secrets are encrypted only when rewritten in /root/kcsa/rewrite.txt.
  4. Apply PSA enforce=restricted and enforce-version=v1.36 to app. Grant the runner SA only pods get, list, and create in app, and do not allow secrets get. Write the leak Pod in namespace app to /root/kcsa/leak.yaml. Use restartPolicy=Never, the runner SA, automatic token mounting turned off, runAsNonRoot with UID 65532, RuntimeDefault seccomp, allowPrivilegeEscalation=false, and capabilities drop ALL. One container runs command: [sha256sum, /s/password] with busybox:1.36 and mounts the db Secret volume s read-only at /s. Do not substitute administrator creation; create it with --as=system:serviceaccount:app:runner. After confirming Succeeded, in /root/kcsa/escalate.txt write direct_secret=no, create_pod=yes, psa=restricted, secret_mount=readable, one hash line from the actual log, and an explanation of the runtime settings PSA blocks and the data access boundary it does not block. Do not print the synthetic secret's original value.
  5. Knock on the apiserver as anonymous and as the default SA, and put the authentication boundary in /root/kcsa/anon.txt.
  6. Write an audit policy to /etc/kubernetes/audit.yaml. In the first rule, protect core secrets, configmaps, and serviceaccounts/token at Metadata with no user, verb, or namespace restriction. Next, only /healthz, /readyz, /livez and their /* subpaths are None, and the last is an unconditional Metadata. omitStages may omit only RequestReceived. Use 3 rules in total, or 4 rules with a detailed RBAC write rule added before the default. The optional detailed rule records only create, update, patch, delete, and deletecollection on roles, rolebindings, clusterroles, and clusterrolebindings in rbac.authorization.k8s.io at RequestResponse. In /etc/rancher/k3s/config.yaml.d/90-labhub-audit.yaml, add the audit arguments with kube-apiserver-arg+ to preserve the existing encryption configuration, and restart k3s. Limit the log to /var/log/kubernetes/audit.log, blocking mode, and maxsize=5, maxbackup=1, maxage=1. When ready, use the provided helper python3 /opt/fixtures/audit-evidence.py capture --policy /etc/kubernetes/audit.yaml --log /var/log/kubernetes/audit.log --case /root/kcsa/audit-case.json --events /root/kcsa/audit-evidence.jsonl to collect the synthetic Secret's create 201, get 200, denied 403, and delete 200. Preserve the four body-less Metadata events and the policy fingerprint, and explain the results in /root/kcsa/audit.txt. The helper does not fix the policy and puts in no real secret.
  7. Summarize everything so far with 4C and STRIDE and put it in /root/kcsa/model.txt.
  8. In /root/kcsa/report.md, write the three lines plaintext_found=yes, encrypted=yes, and audit_lines=, with an explanation.

Notes

This is a 70-minute lab, so extend with the +time button before it expires (up to 180 minutes). Your work disappears when the session ends.

base64 is not encryption

Create a secret and pull it out of the store in plaintext, and put it in /root/kcsa/plaintext.txt.

Open the store with sqlite3 and look for the plaintext in hex(value).

Turn on encryption at rest

Turn on encryption at rest and put the fact that the plaintext disappears in /root/kcsa/encrypt.txt.

Write an EncryptionConfiguration and pass it to the apiserver with --encryption-provider-config.

Old secrets change only when rewritten

Put the fact that old secrets are encrypted only when rewritten in /root/kcsa/rewrite.txt.

Rewrite them with kubectl get secret -o json | kubectl replace -f -.

A Restricted Pod also reads a Secret volume

Apply PSA enforce=restricted and enforce-version=v1.36 to app. Grant the runner SA only pods get, list, and create in app, and do not allow secrets get. Write the leak Pod in namespace app to /root/kcsa/leak.yaml. Use restartPolicy=Never, the runner SA, automatic token mounting turned off, runAsNonRoot with UID 65532, RuntimeDefault seccomp, allowPrivilegeEscalation=false, and capabilities drop ALL. One container runs command: [sha256sum, /s/password] with busybox:1.36 and mounts the db Secret volume s read-only at /s. Do not substitute administrator creation; create it with --as=system:serviceaccount:app:runner. After confirming Succeeded, in /root/kcsa/escalate.txt write direct_secret=no, create_pod=yes, psa=restricted, secret_mount=readable, one hash line from the actual log, and an explanation of the runtime settings PSA blocks and the data access boundary it does not block. Do not print the synthetic secret's original value.

The requesting subject and the Pod's SA are different. Create it as runner, and distinguish the PSA runtime conditions from the Secret reference.

Knock on the authentication boundary

Knock on the apiserver as anonymous and as the default SA, and put the authentication boundary in /root/kcsa/anon.txt.

Try calling the apiserver directly with an anonymous request and with the default SA token.

If you do not record it, it never happened

Write an audit policy to /etc/kubernetes/audit.yaml. In the first rule, protect core secrets, configmaps, and serviceaccounts/token at Metadata with no user, verb, or namespace restriction. Next, only /healthz, /readyz, /livez and their /* subpaths are None, and the last is an unconditional Metadata. omitStages may omit only RequestReceived. Use 3 rules in total, or 4 rules with a detailed RBAC write rule added before the default. The optional detailed rule records only create, update, patch, delete, and deletecollection on roles, rolebindings, clusterroles, and clusterrolebindings in rbac.authorization.k8s.io at RequestResponse. In /etc/rancher/k3s/config.yaml.d/90-labhub-audit.yaml, add the audit arguments with kube-apiserver-arg+ to preserve the existing encryption configuration, and restart k3s. Limit the log to /var/log/kubernetes/audit.log, blocking mode, and maxsize=5, maxbackup=1, maxage=1. When ready, use the provided helper python3 /opt/fixtures/audit-evidence.py capture --policy /etc/kubernetes/audit.yaml --log /var/log/kubernetes/audit.log --case /root/kcsa/audit-case.json --events /root/kcsa/audit-evidence.jsonl to collect the synthetic Secret's create 201, get 200, denied 403, and delete 200. Preserve the four body-less Metadata events and the policy fingerprint, and explain the results in /root/kcsa/audit.txt. The helper does not fix the policy and puts in no real secret.

The protection rule must match first. omitStages is not body removal. Distinguish writing the policy from collecting the actual completed events.

Summarize with 4C and STRIDE

Summarize everything so far with 4C and STRIDE and put it in /root/kcsa/model.txt.

Map each attack you have seen so far to a 4C layer and a STRIDE category.

What you learned

In /root/kcsa/report.md, write the three lines plaintext_found=yes, encrypted=yes, and audit_lines=, with an explanation.

Write the three lines plaintext_found=yes, encrypted=yes, and audit_lines=, with an explanation.