KCSA — Kubernetes Security Associate
The Auditor Arrives, and There's No Proof It's Switched On
Goal
Directly verify several security control items (blocking anonymous access, enforcing Pod Security, disabling automatic service account token mounting, and resource quotas)
in the cluster, and leave the results as a per-control-ID pass/fail report and a gap list.
Why it matters
Frameworks such as the CIS Kubernetes Benchmark, the NSA/CISA hardening guide, and SOC 2 and ISO 27001 are in the end
lists of questions asking "is this control turned on?" What becomes a problem in an audit is not so much a missing control as
a control recorded as on but actually off. So every line of the report must come not from a configuration file or
memory but from the answer the cluster returns right now (auth can-i, object fields, and labels).
Also, you must not stop after looking only at what you hardened. Finding the places left outside control, like the default namespace that nobody touched,
and recording them as gaps is half of a compliance inspection.
Steps
- Create the namespace
kcsa-compto be inspected. - Ask whether
system:anonymouscan list secrets and record it inanon.txt. - Attach the label
pod-security.kubernetes.io/enforce=restrictedtokcsa-comp. - Create a Pod
hardenedthat meets all the restricted requirements. - Turn off automatic token mounting for the
defaultservice account. - Set the namespace resource limits with the ResourceQuota
quota. - Write the actual results of the four control items as the report
compliance.csv. - Find the namespaces that have no enforce label and record them as gaps in
gaps.txt.
Notes
- Keep all artifacts under
/root/kcsa-comp/. The grader compares the values in the files with the cluster's actual state again. kubectl auth can-i ... --as=<사용자>(the placeholder is the user) answers only when that subject can create a SelfSubjectAccessReview. For a subject that cannot even do that, like an anonymous user, an administrator asks on its behalf with a SubjectAccessReview.kubectl get ns -L <라벨키>(the placeholder is the label key) lets you see each namespace's labels in one table.- Official documentation: Pod Security Standards · Auditing · Authorization and SubjectAccessReview.
Set up the namespace to audit
Create the namespace kcsa-comp that will be the target of the compliance inspection.
An audit starts with setting the scope. If you generate the namespace create with --dry-run=client and apply it, it is safe to run several times.
Can someone who has not logged in see secrets
Ask the cluster directly whether an unauthenticated user (system:anonymous) can list secrets in the namespace kcsa-comp, and write that answer to /root/kcsa-comp/anon.txt as one line anonymous-list-secrets=<yes|no>.
First try kubectl auth can-i with --as=system:anonymous. You get a refusal instead of an answer — can-i creates a SelfSubjectAccessReview as that user itself, and an anonymous user cannot even do that. So create a SubjectAccessReview (authorization.k8s.io/v1), with which an administrator asks on its behalf, fill in spec.user, groups (system:unauthenticated), and resourceAttributes, and read status.allowed. If false, it is no.
Put a restricted gatekeeper on the namespace
Attach the Pod Security Admission label pod-security.kubernetes.io/enforce=restricted to the namespace kcsa-comp (along with the warn and audit labels if you like).
Pod Security Standards are turned on with a single namespace label. The label key is pod-security.kubernetes.io/<모드> (the placeholder is the mode) and the value is one of privileged, baseline, and restricted. If you add --overwrite to kubectl label, it is safe to run again.
Admit a Pod that passes the restricted standard
Create a Pod hardened (image nginx:1.27-alpine) in the namespace kcsa-comp. Declare runAsNonRoot: true and seccompProfile.type: RuntimeDefault at the Pod level, and allowPrivilegeEscalation: false, capabilities.drop: [ALL], and runAsNonRoot: true at the container level, to meet all the requirements of the restricted profile.
The restricted profile requires not running as root, blocking privilege escalation, dropping all capabilities, and specifying a seccomp profile. securityContext exists in two places, the Pod level (spec.securityContext) and the container level (containers[].securityContext), and the places where each field may go differ. If even one requirement is missing, creation may be rejected because of the label from step 3.
A token you never use is plugged into every Pod
Set automountServiceAccountToken: false on the default service account of the namespace kcsa-comp, so that API tokens are not automatically mounted into Pods that did not separately request one.
When a namespace is created, a controller creates the default service account for you. If it is not there yet, wait a moment and check. This field is a top-level field of the service account object, so you can change it with patch.
So that one team cannot eat the whole cluster
Create a ResourceQuota quota in the namespace kcsa-comp and set pods=10, requests.cpu=2, and requests.memory=2Gi as the limits.
A ResourceQuota is an object that limits the total for the whole namespace, and you write the resource names and limits under spec.hard. With kubectl create quota, you can join several items with commas using --hard.
The control item report to hand to the auditor
Create /root/kcsa-comp/compliance.csv. The first line is the header control,status, and below it write the four control items anonymous-access, psa-enforce, sa-token-automount, and resource-quota, one per line, with the status holding the result you actually confirmed (pass or fail). All four items must be in the pass state.
The values in a report are evidence, not declarations. The grader rechecks the corresponding control in the cluster for each line and fails you if the written status differs from the actual state. Think about which control item each result you saw in steps 2, 3, 5, and 6 corresponds to, and check once more before writing.
Who is guarding the default namespace
A namespace without a PSA enforce label is a compliance gap. Check whether the cluster's default namespace has an enforce label, and write the name of the namespace with the gap to /root/kcsa-comp/gaps.txt as one line psa-missing-namespace=<네임스페이스> (the placeholder is the namespace). (Do not fix it by attaching the label — this step is for recording a finding.)
To see the labels of all namespaces at once, use the option that adds a label column to kubectl get ns. If you put the namespace you just hardened next to the namespace nobody touched and compare, the gap shows.