KCSA — Kubernetes Security Associate
Secret Access, Pod Creation, and Runtime Security Are Separate Boundaries
In one line
The permission to read the Secret API, the permission to create Pods, and a Pod's runtime security settings are different boundaries. A Restricted Pod can also read a Secret in the same namespace as a volume.
Why this was needed
Here is an example scenario. get secrets was removed from a deployment service account.
kubectl auth can-i also answered no, so it was reported that the secret could not be accessed.
But that account still had create pods. Instead of the API that reads Secrets, the attacker chose the API
that creates a Pod with a Secret volume. Once the kubelet prepared the volume, the
container obtained the value through an ordinary file read. A direct access denial and blocking indirect access are different.
How it works
Three questions to ask
| Boundary | The question it asks | What this lab looks at |
|---|---|---|
| Authentication | Who is the requester? | Distinguishing the administrator certificate from the runner SA requested by proxy |
| Authorization | Does it allow this verb, resource, and scope? | Secret lookup is denied, and the app's Pod creation is allowed |
| Admission | Will it accept the object of an allowed create request? | Checks whether the Pod meets the Restricted runtime conditions |
The subject of a Pod creation request and the Pod's serviceAccountName are not the same concept.
Just because an administrator created a Pod that specifies the runner does not mean the runner's create permission was used.
The next lab sends an actual create request with --as=system:serviceaccount:app:runner.
This is a proxy request for teaching that uses the administrator's impersonate permission, not theft or forgery of an SA token.
What Restricted blocks and what it leaves
Restricted in the Pod Security Standards limits container runtime settings such as privilege escalation and running as root.
From baseline on, it limits dangerous settings such as hostPath, hostPID, hostNetwork, and privileged. But the volume types Restricted allows include secret and configMap.
This policy does not judge whether a given user may use a specific Secret for business purposes.
Even if you put in all of runAsNonRoot: true, capabilities drop ALL, allowPrivilegeEscalation: false,
and seccompProfile: RuntimeDefault, reading a file that the container legitimately had mounted
is not forbidden. automountServiceAccountToken: false is also an option that turns off automatic API token
mounting, not an option that blocks Secret volumes.
So the explanation that create pods is always node root is wrong too. Even with the same permission, the reach differs
depending on the admission policy, the SAs that can be selected, the namespace scope, and the volume types.
pods/exec is affected by the permissions and data of the existing container you can get into.
bind, escalate, and impersonate also require checking the allowed resources and subject scope, and
you must not flatly conclude that it is unconditionally cluster-admin just because one of these verbs is present.
Defense sets the boundary of data and workloads together
The starting point is not to put mutually untrusting workloads in the same namespace as sensitive Secrets.
Minimize the permission to create workloads and the service accounts that can be selected, and if needed,
restrict the allowed Secret references and SA selection with a separate admission policy. Even if you put
resourceNames on the direct get permission, the volumes referenced through Pod creation are not automatically restricted.
A NetworkPolicy is a control on network connections, so it does not block reading local files that are already mounted.
PSA is still needed on top of that. You must apply together the control that reduces dangerous runtime settings that lead to the node and the control that decides who can use business data inside a namespace. The key is not to skip verifying another boundary just because you have a checkmark that one was turned on.
Encryption at rest is also a different boundary
The API data value of a Secret is a base64 representation and is not encryption. If encryption at rest is not
configured, the data in the store has no confidentiality guarantee. In this k3s/kine environment,
we check for a synthetic marker in the serialized Secret inside SQLite. This does not mean that every distribution uses the same file or
serialization format, and you must separately check the encryption defaults of managed services.
EncryptionConfiguration writes new values with the first provider and reads old values with the later providers. If you put identity first, even new data is not encrypted. Turning on the setting or changing the key also does not automatically resave existing objects. You need rewriting, verification, and removal of the old read path. A properly authorized API request receives the decrypted value, so encryption at rest does not block the mount path above. If you expose the key and backups together, you can also lose the effect of encryption.
What it looks like in the field
An example inspection order is as follows. First, for a specific SA, specify the namespace and ask
can-i get secrets and can-i create pods separately. Next, look at which Pods can be created
through the admission policy and the SA selection scope. Finally, check what data the created workloads
mount. can-i --list is a starting point and is not evidence that fully explains every external authorization and admission
result. You must look at the actual request and the cause of failure together.
Even if you turn off automatic token mounting on a service account, settings explicitly specified in the Pod may take precedence. Fixing this option does not immediately change every Pod already running, so check the impact and apply it with new Pods. Judge separately whether a token is unneeded and whether a Secret is unneeded.
Read 401 and 403 separately as well. An unqualified request on a path where anonymous authentication is allowed may be
processed as the system:anonymous user and the system:unauthenticated group.
So an anonymous request is not always a 401. It varies with per-path anonymous allowance, invalid credentials, and
the authorization result. Do not conclude from a 403 alone which admission controls it passed;
check the reason in the response and the audit record together.
What you will do in the next lab
After confirming encryption at rest and rewriting with a synthetic Secret, you compare the hashes of the Secret file in a non-root Pod that the runner created in the Restricted-enforced app. The actual secret value is not printed. You record the API direct lookup denial, the Pod creation success, and the Secret volume read success as separate pieces of evidence. Then you check the authentication boundary and an audit log without bodies. This VM is a single node for teaching, and you must not extend these results into a security guarantee for other tenants or an entire production cluster.