PSP 폐지 후 Pod Security Admission으로 특권 컨테이너 차단하기
K8s 보안 심화 가이드 2편 — 1편에서 NetworkPolicy로 '네트워크 격리'를 끝냈다면, 이번엔 '워크로드 권한 제어' 차례입니다.
PSP는 사라졌는데 특권 컨테이너는 그대로?
쿠버네티스 v1.21에서 deprecated 되었던 PodSecurityPolicy(PSP)는 v1.25에서 완전히 제거됐습니다. 문제는, PSP를 쓰던 클러스터를 업그레이드하면서 "차단 장치"가 통째로 사라졌다는 점입니다. 그 결과 privileged: true, hostPath 마운트, root 실행 같은 위험한 Pod이 아무 제지 없이 떠버리는 클러스터가 의외로 많습니다. 한 번이라도 워커 노드 권한이 컨테이너 탈출(container escape)로 뚫리면, 노드 위 모든 워크로드가 위험해집니다.
그 빈자리를 채우는 빌트인 표준이 바로 Pod Security Admission(PSA) 입니다. v1.25에서 GA로 승격되어 별도 설치 없이 API 서버에 내장돼 있고, namespace 라벨 한 줄로 켤 수 있습니다. CIS Kubernetes Benchmark와 공급망 보안 강화 흐름과 맞물리며 restricted 프로파일이 사실상 기본 권장값으로 자리 잡았죠.
실무 경험상, PSA는 "전부 막는 도구"가 아니라 단계적으로 켜는 가드레일로 접근해야 깨짐 없이 정착됩니다. 이 글에서 그 절차를 복붙 가능한 형태로 정리합니다.
PSA 한눈에 정리: 3모드 × 3등급
PSA는 namespace 단위로 동작하며, 모드(어떻게 반응할지) 와 등급(얼마나 엄격할지) 의 조합으로 정책을 정합니다.
3가지 모드
| 모드 | 동작 | 용도 |
|---|---|---|
enforce | 위반 Pod 생성 거부 | 실제 차단 (운영 적용) |
audit | 거부하지 않고 audit 로그에만 기록 | 위반 현황 파악 |
warn | 거부하지 않고 kubectl에 경고 표시 | 사용자 즉시 피드백 |
세 모드는 동시에 적용할 수 있습니다. 예를 들어 enforce=baseline + audit=restricted로 두면, baseline은 강제하면서 restricted 위반은 로그로 추적하는 식의 단계적 운영이 가능합니다.
3가지 등급과 금지 항목
| 항목 | privileged | baseline | restricted |
|---|---|---|---|
privileged: true | 허용 | ❌ 금지 | ❌ 금지 |
hostNetwork / hostPID / hostIPC | 허용 | ❌ 금지 | ❌ 금지 |
hostPath 볼륨 | 허용 | ❌ 금지 | ❌ 금지 |
| 위험 capabilities 추가 | 허용 | 일부만 | ❌ (drop ALL 필요) |
allowPrivilegeEscalation: false | 불필요 | 불필요 | ✅ 필수 |
runAsNonRoot: true | 불필요 | 불필요 | ✅ 필수 |
seccompProfile: RuntimeDefault | 불필요 | 불필요 | ✅ 필수 |
요약하면 privileged = 무제한, baseline = 알려진 권한 상승 차단(최소 제한), restricted = 현행 보안 모범사례(강력 제한) 입니다. PSA는 admission controller 단계에서 Pod 스펙을 검사하므로, 정책에 어긋나면 객체가 etcd에 저장되기도 전에 거부됩니다.
실제로 막아보기: namespace 라벨 적용
테스트용 네임스페이스를 만들고 restricted enforce를 켭니다.
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-labelsaudit/warn까지 동시에 거는 권장 패턴:
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이제 특권 Pod을 만들어 봅니다.
# 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: /kubectl apply -f bad-pod.yaml실제 출력되는 거부 메시지는 다음과 같습니다.
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")각 줄 해석:
- privileged — 컨테이너가 노드 커널에 거의 무제한 접근. 가장 위험한 항목.
- allowPrivilegeEscalation != false —
setuid등을 통한 권한 상승 가능성을 막지 않음. - unrestricted capabilities — Linux capability를 모두 drop하지 않음.
- restricted volume types: hostPath — 노드 파일시스템 직접 마운트(여기선
/루트까지) 시도. - runAsNonRoot != true — root(UID 0) 실행 가능성.
- seccompProfile — 시스템콜 필터링 미적용.
통과시키기: restricted를 만족하는 securityContext
위 위반을 하나씩 해소하면 됩니다. Before/After로 비교해 보죠.
Before (거부됨):
spec:
containers:
- name: app
image: nginx:1.27
securityContext:
privileged: true
volumes:
- name: host
hostPath:
path: /After (통과):
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로 교체포인트는 (1) root로 떠야 하는 기본 nginx 대신 비특권 이미지를 쓰고, (2) hostPath를 emptyDir/configMap/PVC 같은 허용 볼륨으로 바꾸고, (3) Pod·container 양쪽에 securityContext 4종 세트(runAsNonRoot, allowPrivilegeEscalation:false, drop ALL, seccompProfile)를 채우는 것입니다.
결론: 깨짐 없이 적용하는 단계적 마이그레이션
기존 클러스터에 enforce=restricted를 곧바로 켜면 운영 워크로드가 무더기로 거부됩니다. 다음 순서를 권합니다.
# ① 먼저 경고/감사만 — 차단하지 않고 위반만 노출
kubectl label namespace prod \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
# (kubectl apply/rollout 시 warning 메시지로 위반 워크로드 즉시 식별)- warn + audit만 적용 → 어떤 워크로드가 걸리는지 식별
- audit 로그 확인 → API server audit 로그에서
pod-security.kubernetes.io/audit-violations어노테이션으로 위반 집계 - securityContext 수정 → 위 After 예제대로 깃옵스 매니페스트 일괄 정비
- enforce 승격 → 마지막에
enforce=restricted로 전환
클러스터 전역 기본값이 필요하면 API 서버의 AdmissionConfiguration에서 PodSecurity 플러그인 defaults(enforce/audit/warn 기본 등급)와 exemptions를 설정할 수 있습니다. 라벨이 없는 네임스페이스에도 일괄 적용돼 더 안전합니다.
마지막으로, PSA는 빌트인 기본 가드레일입니다. "이미지 레지스트리 제한", "특정 라벨 강제" 같은 세밀한 커스텀 규칙은 Kyverno나 OPA Gatekeeper로 보완하는 역할 분담이 정석입니다.
다음 편 예고 (3편): RBAC 최소 권한 설계 — 누가 무엇을 할 수 있는지를 좁혀 공격 표면을 줄입니다.
자주 묻는 질문 (FAQ)
Q. 모니터링 에이전트처럼 꼭 hostPath/privileged가 필요한 네임스페이스는 어떻게 예외 처리하나요?
A. 해당 네임스페이스에만 pod-security.kubernetes.io/enforce=privileged 라벨을 주거나, 라벨을 아예 적용하지 않으면 됩니다. 전역 기본값을 강하게 둔 경우엔 AdmissionConfiguration의 exemptions.namespaces에 해당 네임스페이스를 등록해 제외합니다. 단, 예외는 최소한으로 두고 별도 추적하세요.
Q. Deployment나 Job이 만든 Pod이 거부되면 왜 명령어에선 성공처럼 보이나요?
A. Deployment/Job 객체 자체는 정상 생성되고, 실제 위반은 ReplicaSet/Job이 Pod을 만드는 시점에 발생합니다. 그래서 kubectl get deploy는 정상으로 보여도 Pod이 안 뜹니다. kubectl get events -n <ns>와 kubectl describe rs <replicaset>로 FailedCreate와 violates PodSecurity 메시지를 확인하면 원인 컨테이너와 위반 항목을 정확히 잡을 수 있습니다.
Q. enforce 등급을 올렸다가 장애가 나면 즉시 되돌릴 수 있나요?
A. 네. enforce는 라벨 기반이라 kubectl label ns <ns> pod-security.kubernetes.io/enforce=baseline --overwrite로 즉시 완화됩니다. 다만 이미 거부된 Pod은 재생성(rollout)이 필요합니다.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...