TT Lab
Get started
Learn Learning paths Courses

CKS — Kubernetes Security Specialist

Why Three Lines of Labels Replaced PSP

Continue in TT Lab

In one line

Pod Security Admission sets a security floor for workloads with a few lines of namespace labels. And a Secret is only base64-encoded, not encrypted, so it needs separate controls.

Why this was needed

PodSecurityPolicy was deprecated in 1.21 and removed in 1.25. There were two reasons. To use a PSP, you had to give the workload's ServiceAccount the RBAC permission to "use this PSP," and that structure itself created a privilege escalation path. And it was very hard to predict which PSP would apply to which Pod, because the rules for choosing one of several PSPs were complicated.

Its replacement, PSA, is simple in exactly the opposite way. You just attach labels to the namespace.

pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.30
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted

enforce rejects violating Pods, audit records them only in the audit log, and warn only shows a warning to the kubectl user. Being able to set the three modes separately is the heart of migration. If you apply enforce: restricted straight to an existing cluster, workloads get blocked in bulk, so you turn on warn and audit first, observe what gets caught, and then raise to enforce.

The -version labels matter too. The profile definitions get slightly stricter with each Kubernetes version. If you pin the version, you can prevent a cluster upgrade from turning into a policy tightening that suddenly rejects Pods.

How it works

There are three profiles.

Profile Key restrictions
privileged No restrictions. For system workloads like CNI and storage drivers
baseline No hostNetwork/hostPID/hostIPC, no privileged, no hostPath, no dangerous capabilities
restricted baseline + runAsNonRoot required, seccompProfile required, capabilities drop ALL required, allowPrivilegeEscalation false required

A Pod that passes restricted always has four fields. runAsNonRoot: true, allowPrivilegeEscalation: false, capabilities.drop: ["ALL"], and seccompProfile.type: RuntimeDefault. If even one of the four is missing, it is rejected, and the error message says as is which field was caught and why.

The second axis is the Secret. It is the part practitioners misunderstand most often. The values in a Secret manifest are in base64, so they look encrypted, but base64 is an encoding, not encryption. There is no key, and reversing it takes one command. By default, Secrets are stored in etcd in plaintext, so anyone who gets an etcd backup file or a disk image reads every Secret. Encryption at rest is turned on by creating an EncryptionConfiguration file and specifying it with kube-apiserver's --encryption-provider-config. Here the identity provider must be last in the list. If it comes first, it goes back to plaintext storage. Even after you change the configuration, existing Secrets stay in plaintext until they are rewritten, so you must update them all once.

The third is the injection method. If you inject it as an environment variable, it can be read through /proc/<PID>/environ, it is inherited by child processes, and it is carried out wholesale in crash reports and debug pages. A volume mount is safer, and allowing only owner read with defaultMode: 0400 is the convention.

What it looks like in the field

If you open etcd directly, the misunderstanding disappears. With encryption off, running etcdctl get /registry/secrets/default/app-db | hexdump -C shows the key path and value as they are. With encryption on, a provider prefix like k8s:enc:kms:v2: is attached in the same place and the body becomes unreadable. The designs of people who have seen this difference with their own eyes and those who haven't are different.

Misunderstandings on the RBAC side are also common. A person with get secrets permission in a namespace reads every Secret in that namespace. It is very common for an edit permission given to developers as a convenience to be effectively permission to view production credentials. You can check it with the single line kubectl auth can-i get secrets --namespace payments --as dev@example.com, but few teams check. It is also not well known that there is a way to allow only a specific Secret with a Role's resourceNames.

The same story repeats from the multi-tenancy angle. Namespace-based isolation shares the API server, so CRD installation conflicts and the noisy neighbor problem remain. Approaches like vCluster that give each tenant an independent API server solve this problem, but the workload Pods are ultimately materialized in a namespace of the host cluster in the form <vcluster>-x-<이름>-x-<네임스페이스> (the placeholders are the name and the namespace). That is, PSS and NetworkPolicy hygiene on the host side is still needed. Adding one more layer of abstraction doesn't exempt the controls on the layer below.

What you will do in the next lab

First you apply baseline and restricted to two namespaces respectively, create a Pod that passes restricted, and then confirm that a violating Pod is actually rejected. Then the Secret lab covers creating by type, the difference between environment variable injection and volume mounts, defaultMode, immutable Secrets, RBAC narrowed with resourceNames, and writing an EncryptionConfiguration file.