Pod Security Admission Namespace Labels
One label on a namespace turns a privileged Pod from Running into Forbidden. Then a compliant Pod goes in, and touch /root fails inside it - proving the constraint is real at runtime, not just at admission.
Security and the 4C Model Guide 24 of 46 Beginner
- Kubernetes1.36.4
- AuthorizationNode,RBAC
- Admission pluginsNodeRestriction (explicit) plus the defaults
- TimeAbout 17 min
- Reviewed22 August 2026
Written against the versions above. The restricted profile tightens over time, which is what `enforce-version=latest` opts into. Pinning a version is the alternative and has its own cost.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA1001 | 192.168.0.175 | Ubuntu 26.04 LTS | Control Plane Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE02 | 192.168.0.177 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- A cluster on 1.25 or later. Pod Security admission is built in - nothing to install.
- Cluster-admin, to label namespaces.
-
What the API server enforces by default
Read the API server's own flags first, because the answer shapes everything else:
--authorization-mode=Node,RBAC --enable-admission-plugins=NodeRestrictionNode,RBACis who may ask.NodeRestrictionis one explicitly enabled admission plugin - it stops a kubelet from editing objects belonging to other nodes.What is not there is any Pod-level restriction. A default kubeadm cluster will accept a privileged Pod in any namespace, and that is a deliberate default: Kubernetes ships permissive and expects you to tighten it.
bash Example session sudo -n grep -E 'enable-admission-plugins|--authorization-mode' /etc/kubernetes/manifests/kube-apiserver.yaml - --authorization-mode=Node,RBAC - --enable-admission-plugins=NodeRestrictionExpected resultThe authorization mode and the explicitly enabled admission plugins.
Success conditionYou can state what your API server enforces before you change anything.
-
A privileged Pod, in a namespace with no rules
privileged: trueis the strongest thing you can ask for. It disables essentially every container isolation feature - the process gets all capabilities, and device access to the host.In an unlabelled namespace it is simply created, and it reaches Running. Nothing warns you.
This is what an unlabelled namespace means in practice, and it is why namespace-level policy is the first thing to add to a cluster that more than one team can deploy to.
bash Example session kubectl --context cka1001 create namespace psa-opennamespace/psa-open createdkubectl --context cka1001 -n psa-open run rooty --image=busybox:1.37 --restart=Never --overrides='{"spec":{"containers":[{"name":"rooty","image":"busybox:1.37","command":["sleep","3600"],"securityContext":{"privileged":true}}]}}'pod/rooty createdExpected resultThe Pod created, with
privileged: trueaccepted.Success condition
pod/rooty created. -
It really is running, and really is privileged
Worth confirming rather than assuming - the earlier check caught it mid image pull. Settled, it reads
RunningwithPRIV true.A privileged container on a node is, for practical purposes, root on that node. Any workload that can create Pods in this namespace can therefore own the node.
bash Example session kubectl --context cka1001 -n psa-open get pod rooty -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,PRIV:.spec.containers[0].securityContext.privilegedNAME STATUS PRIVrooty Running trueExpected result
Runningandtrue.Success conditionThe privileged Pod is running.
-
One label changes the answer
Two labels, really: which profile to enforce, and which version of it.
The same
kubectl runnow fails at admission:Error from server (Forbidden): pods "rooty" is forbidden: violates PodSecurity "restricted:latest": privilegedNote where it failed. Not scheduled and killed - rejected by the API server, so nothing was ever created.
get podsconfirms the namespace is empty.The three profiles are
privileged(no restrictions),baseline(blocks known escalations) andrestricted(the hardened profile used here). And three modes:enforcerejects,auditrecords,warntells the person applying. Rolling out withwarnbeforeenforceis how you avoid breaking a namespace you do not own.bash Example session kubectl --context cka1001 create namespace psa-strictnamespace/psa-strict createdkubectl --context cka1001 label namespace psa-strict pod-security.kubernetes.io/enforce=restricted pod-security.kubernetes.io/enforce-version=latestnamespace/psa-strict labeledkubectl --context cka1001 -n psa-strict run rooty --image=busybox:1.37 --restart=Never --overrides='{"spec":{"containers":[{"name":"rooty","image":"busybox:1.37","command":["sleep","3600"],"securityContext":{"privileged":true}}]}}'Error from server (Forbidden): pods "rooty" is forbidden: violates PodSecurity "restricted:latest": privileged (container "rooty" must not set securityContext.privileged=true), allowPrivilegeEscalation != false (container "rooty" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "rooty" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "rooty" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "rooty" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")[exit 1]kubectl --context cka1001 -n psa-strict get podsNo resources found in psa-strict namespace.Expected resultA
Forbiddennaming the violated profile and the specific violation, then an empty namespace.Success conditionThe Pod is rejected rather than created.
-
A Pod that satisfies restricted
The restricted profile is not vague about what it wants, and the list is short enough to memorise: no privilege escalation, run as non-root, drop all capabilities, and a seccomp profile.
This Pod declares all four and is admitted.
runAsUser: 1000withcapabilities.drop: [ALL]is most of the work.bash Example session kubectl --context cka1001 apply -f - <<'YAML'apiVersion: v1kind: Podmetadata: name: tidy namespace: psa-strictspec: containers: - name: app image: busybox:1.37 command: ["sleep", "3600"] securityContext: allowPrivilegeEscalation: false runAsNonRoot: true runAsUser: 1000 capabilities: drop: ["ALL"] seccompProfile: type: RuntimeDefaultYAMLpod/tidy createdkubectl --context cka1001 -n psa-strict get pod tidy -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,USER:.spec.containers[0].securityContext.runAsUserNAME STATUS USERtidy Pending 1000Expected resultThe Pod created and, once settled, Running as uid 1000 with all capabilities dropped.
Success conditionA Pod exists in the restricted namespace.
-
The constraint holds at runtime too
Admission is a gate at creation time. This is the part that proves the gate bought something real.
idinside the container reportsuid=1000, and writing to/rootfails:touch: /root/nope: Permission deniedThe kernel is enforcing it, not Kubernetes. Admission only guaranteed the Pod *asked* to be unprivileged; the isolation itself comes from the uid and the dropped capabilities.
One detail worth noticing:
gid=0(root). Running as a non-root user does not mean a non-root group, and the restricted profile does not require it. Files group-writable by root are still writable here - a real gap people miss.bash Example session kubectl --context cka1001 get ns psa-strict -o jsonpath='{.metadata.labels}{"\n"}'{"kubernetes.io/metadata.name":"psa-strict","pod-security.kubernetes.io/enforce":"restricted","pod-security.kubernetes.io/enforce-version":"latest"}kubectl --context cka1001 -n psa-strict exec tidy -- iduid=1000 gid=0(root) groups=0(root)kubectl --context cka1001 -n psa-strict exec tidy -- sh -c "touch /root/nope 2>&1 || true"touch: /root/nope: Permission deniedExpected resultThe namespace labels, uid 1000 with gid 0, and a permission denial.
Success conditionThe write fails from inside the container.
Troubleshooting
Existing Pods keep running after you label a namespace.
Why: Pod Security admission is a create/update gate. It never evicts.
Fix:Label, then recreate the workloads.
kubectl rollout restarton the Deployments is usually enough, and anything that fails to come back was violating the profile.A Deployment silently creates no Pods after labelling.
Why: The ReplicaSet controller is being rejected, and the error lands on the ReplicaSet, not the Deployment.
Fix:
kubectl describe rs <name>and read Events. This is the single most confusing PSA symptom, becauseget podsshows nothing at all.enforce-version=latestbroke a namespace after an upgrade.Why:
latesttracks the cluster version, and the restricted profile gains requirements.Fix:Pin to a version,
enforce-version=v1.32, and move it deliberately.latestis right for a lab and risky for a shared cluster.