CertGrid CertGrid
Hands-on Lab·Kubernetes and Cloud Native Associate

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

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.

Pod Security admission is built into the API server, so nothing needs installing for any of this.
Server NameIP AddressOSRolesCPURAMHDD
CKA1001192.168.0.175Ubuntu 26.04 LTSControl Plane Node2 Core4 GB50 GB
CKA1001-NODE02192.168.0.177Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB

Before you start

  1. 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=NodeRestriction

    Node,RBAC is who may ask. NodeRestriction is 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=NodeRestriction

    Expected resultThe authorization mode and the explicitly enabled admission plugins.

    Success conditionYou can state what your API server enforces before you change anything.

  2. A privileged Pod, in a namespace with no rules

    privileged: true is 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 created

    Expected resultThe Pod created, with privileged: true accepted.

    Success conditionpod/rooty created.

  3. It really is running, and really is privileged

    Worth confirming rather than assuming - the earlier check caught it mid image pull. Settled, it reads Running with PRIV 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   true

    Expected resultRunning and true.

    Success conditionThe privileged Pod is running.

  4. One label changes the answer

    Two labels, really: which profile to enforce, and which version of it.

    The same kubectl run now fails at admission:

    Error from server (Forbidden): pods "rooty" is forbidden:
    violates PodSecurity "restricted:latest": privileged

    Note where it failed. Not scheduled and killed - rejected by the API server, so nothing was ever created. get pods confirms the namespace is empty.

    The three profiles are privileged (no restrictions), baseline (blocks known escalations) and restricted (the hardened profile used here). And three modes: enforce rejects, audit records, warn tells the person applying. Rolling out with warn before enforce is 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 Forbidden naming the violated profile and the specific violation, then an empty namespace.

    Success conditionThe Pod is rejected rather than created.

  5. 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: 1000 with capabilities.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   1000

    Expected resultThe Pod created and, once settled, Running as uid 1000 with all capabilities dropped.

    Success conditionA Pod exists in the restricted namespace.

  6. The constraint holds at runtime too

    Admission is a gate at creation time. This is the part that proves the gate bought something real.

    id inside the container reports uid=1000, and writing to /root fails:

    touch: /root/nope: Permission denied

    The 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 denied

    Expected resultThe namespace labels, uid 1000 with gid 0, and a permission denial.

    Success conditionThe write fails from inside the container.

Troubleshooting

Official sources