KCSA — Kubernetes Security Associate
Open the Cluster With an Attacker's Eyes
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
- Create a secret and pull it out of the store in plaintext, and put it in
/root/kcsa/plaintext.txt. - Turn on encryption at rest and put the fact that the plaintext disappears in
/root/kcsa/encrypt.txt. - Put the fact that old secrets are encrypted only when rewritten in
/root/kcsa/rewrite.txt. - Apply PSA
enforce=restrictedandenforce-version=v1.36toapp. Grant therunnerSA only pods get, list, and create inapp, and do not allow secrets get. Write theleakPod 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 runscommand: [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.txtwritedirect_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. - Knock on the apiserver as anonymous and as the default SA, and put the authentication boundary in
/root/kcsa/anon.txt. - Write an audit policy to
/etc/kubernetes/audit.yaml. In the first rule, protect coresecrets,configmaps, andserviceaccounts/tokenatMetadatawith no user, verb, or namespace restriction. Next, only/healthz,/readyz,/livezand their/*subpaths areNone, and the last is an unconditionalMetadata.omitStagesmay omit onlyRequestReceived. 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 inrbac.authorization.k8s.ioatRequestResponse. In/etc/rancher/k3s/config.yaml.d/90-labhub-audit.yaml, add the audit arguments withkube-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 helperpython3 /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.jsonlto 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. - Summarize everything so far with 4C and STRIDE and put it in
/root/kcsa/model.txt. - In
/root/kcsa/report.md, write the three linesplaintext_found=yes,encrypted=yes, andaudit_lines=, with an explanation.
Notes
- The store is
/var/lib/rancher/k3s/server/db/state.db. Open it directly withsqlite3. - Values go in raw, not as base64. Pull them out with
hex(value)and look for the plaintext. - After you bring the apiserver back up, wait until
kubectl get --raw=/readyzbecomesok. - Common misconception: that if you turn on encryption, old secrets are encrypted too. They change only when rewritten.
- Common misconception: that it is safe if
can-i get secretsis no. You can reach it indirectly through permission to create workloads that allow Secret volumes. Restricted also allows Secret volumes.
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.