/보안/Blocking Privileged Containers with Pod Security Admission After PSP Removal
SecurityPod Security Admission쿠버네티스 보안

Blocking Privileged Containers with Pod Security Admission After PSP Removal

A hands-on guide to configuring Pod Security Admission, the built-in standard after PSP was removed. Block privileged, hostPath, and root with namespace labels, interpret violates PodSecurity errors, pass restricted with the right securityC

Blocking Privileged Containers with Pod Security Admission After PSP Removal

Blocking Privileged Containers with Pod Security Admission After PSP Removal

K8s Security Deep Dive, Part 2 — Part 1 locked down network isolation with NetworkPolicy. This time it is workload privilege control.

PSP Is Gone. Privileged Containers Are Not.

PodSecurityPolicy (PSP), deprecated in Kubernetes v1.21, was fully removed in v1.25. The catch: clusters that had been using PSP lost their entire blocking layer on upgrade. The result is a surprising number of clusters where dangerous Pods—privileged: true, hostPath mounts, running as root—come up with no resistance. If worker-node privileges are ever compromised via container escape, every workload on that node is at risk.

The built-in standard filling that gap is Pod Security Admission (PSA). It went GA in v1.25, ships inside the API server with no extra install, and turns on with a single namespace label. Alongside the CIS Kubernetes Benchmark and supply-chain hardening, the restricted profile has become the de facto recommended default.

In production, PSA is not a “block everything” switch. Treat it as a guardrail you enable in stages so it lands without breakage. This post lays out that procedure in copy-paste form.

PSA at a Glance: 3 Modes × 3 Levels

PSA is namespace-scoped. You set policy as a combination of mode (how it reacts) and level (how strict it is).

The Three Modes

ModeBehaviorUse
enforceReject violating PodsActual blocking (production)
auditDo not reject; record in audit logs onlyDiscover violations
warnDo not reject; show a warning in kubectlImmediate user feedback

All three modes can be applied at the same time. For example, enforce=baseline + audit=restricted enforces baseline while tracking restricted violations in logs—useful for staged rollout.

The Three Levels and What They Ban

Itemprivilegedbaselinerestricted
privileged: trueAllow❌ Ban❌ Ban
hostNetwork / hostPID / hostIPCAllow❌ Ban❌ Ban
hostPath volumesAllow❌ Ban❌ Ban
Adding dangerous capabilitiesAllowSome only❌ (must drop ALL)
allowPrivilegeEscalation: falseNot requiredNot requiredRequired
runAsNonRoot: trueNot requiredNot requiredRequired
seccompProfile: RuntimeDefaultNot requiredNot requiredRequired

In short: privileged = unrestricted, baseline = block known privilege-escalation paths (minimum restriction), restricted = current security best practices (strong restriction). PSA inspects the Pod spec at the admission controller, so objects that violate policy are rejected before they are stored in etcd.

Blocking It for Real: Applying Namespace Labels

Create a test namespace and turn on restricted enforce.

Bash
kubectl create namespace secure-demo

# enforce + 버전 고정 (버전 핀으로 향후 정책 강화 시 불시 차단 방지)
kubectl label namespace secure-demo \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=v1.30

# 라벨 확인
kubectl get ns secure-demo --show-labels

Recommended pattern that also enables audit/warn at the same time:

Bash
kubectl label namespace secure-demo \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/warn-version=v1.30 \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/audit-version=v1.30

Now try creating a privileged Pod.

YAML
# bad-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: bad-pod
  namespace: secure-demo
spec:
  containers:
  - name: app
    image: nginx:1.27
    securityContext:
      privileged: true
  volumes:
  - name: host
    hostPath:
      path: /
Bash
kubectl apply -f bad-pod.yaml

The actual rejection message looks like this.

CODE
Error from server (Forbidden): error when creating "bad-pod.yaml": pods "bad-pod" is forbidden:
violates PodSecurity "restricted:v1.30": privileged (container "app" must not set securityContext.privileged=true),
allowPrivilegeEscalation != false (container "app" must set securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "app" must set securityContext.capabilities.drop=["ALL"]),
restricted volume types (volume "host" uses restricted volume type "hostPath"),
runAsNonRoot != true (pod or container "app" must set securityContext.runAsNonRoot=true),
seccompProfile (pod or container "app" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")

What each line means:

  • privileged — The container gets nearly unrestricted access to the node kernel. The most dangerous item.
  • allowPrivilegeEscalation != false — Does not block privilege escalation via setuid and similar.
  • unrestricted capabilities — Does not drop all Linux capabilities.
  • restricted volume types: hostPath — Attempts to mount the node filesystem directly (here, even / as root).
  • runAsNonRoot != true — May run as root (UID 0).
  • seccompProfile — No syscall filtering applied.

Making It Pass: a securityContext That Satisfies restricted

Fix the violations one by one. Here is a Before/After comparison.

Before (rejected):

YAML
spec:
  containers:
  - name: app
    image: nginx:1.27
    securityContext:
      privileged: true
  volumes:
  - name: host
    hostPath:
      path: /

After (passes):

YAML
apiVersion: v1
kind: Pod
metadata:
  name: good-pod
  namespace: secure-demo
spec:
  securityContext:                # Pod-level
    runAsNonRoot: true
    runAsUser: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: nginxinc/nginx-unprivileged:1.27   # 비특권 포트(8080)로 동작
    ports:
    - containerPort: 8080
    securityContext:              # container-level
      allowPrivilegeEscalation: false
      capabilities:
        drop: ["ALL"]
      runAsNonRoot: true
  volumes:
  - name: cache
    emptyDir: {}                  # hostPath → emptyDir로 교체

The key points: (1) use an unprivileged image instead of default nginx that must run as root, (2) replace hostPath with an allowed volume type such as emptyDir/configMap/PVC, and (3) fill in the four-item securityContext set (runAsNonRoot, allowPrivilegeEscalation: false, drop ALL, seccompProfile) at both Pod and container level.

Wrap-up: Staged Migration Without Breaking Production

Turning on enforce=restricted immediately on an existing cluster will reject production workloads in bulk. Follow this order.

Bash
# ① 먼저 경고/감사만 — 차단하지 않고 위반만 노출
kubectl label namespace prod \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted

# (kubectl apply/rollout 시 warning 메시지로 위반 워크로드 즉시 식별)
  1. Apply warn + audit only → identify which workloads trip the policy
  2. Check audit logs → aggregate violations from API server audit logs via the pod-security.kubernetes.io/audit-violations annotation
  3. Fix securityContext → bulk-update GitOps manifests using the After example above
  4. Promote to enforce → finally switch to enforce=restricted

If you need cluster-wide defaults, configure the PodSecurity plugin defaults (default enforce/audit/warn levels) and exemptions in the API server’s AdmissionConfiguration. That applies even to unlabeled namespaces, which is safer.

Finally, PSA is a built-in baseline guardrail. Fine-grained custom rules such as “restrict image registries” or “require certain labels” are typically complemented by Kyverno or OPA Gatekeeper. That division of labor is the standard approach.

Coming next (Part 3): RBAC least-privilege design — shrink who can do what and cut the attack surface.

FAQ

Q. How do I exempt namespaces that truly need hostPath/privileged, such as monitoring agents? A. Label only that namespace with pod-security.kubernetes.io/enforce=privileged, or do not apply labels at all. If you set strong cluster-wide defaults, register the namespace under exemptions.namespaces in AdmissionConfiguration. Keep exemptions to a minimum and track them separately.

Q. Why does the command look successful when a Pod created by a Deployment or Job is rejected? A. The Deployment/Job object itself is created successfully; the actual violation happens when the ReplicaSet/Job creates the Pod. So kubectl get deploy looks fine even though Pods never come up. Check kubectl get events -n <ns> and kubectl describe rs <replicaset> for FailedCreate and violates PodSecurity to pinpoint the container and the violated items.

Q. If raising the enforce level causes an outage, can I roll it back immediately? A. Yes. Enforce is label-based, so kubectl label ns <ns> pod-security.kubernetes.io/enforce=baseline --overwrite relaxes it immediately. Pods that were already rejected still need to be recreated (a rollout).

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·

Comments

Be the first to comment.