클러스터를 오래 굴리다 보면 v1.25로 올리는 순간 PodSecurityPolicy(PSP)가 통째로 사라져 있는 걸 만난다. 예전에 PSP로 파드 권한을 조이던 매니페스트가 있었는데, 업그레이드 후에 그 리소스 자체가 API에서 없어져서 당황하는 경우가 많다. 제가 현장에서 이 전환을 여러 번 해봤는데, 다행히 대체재인 Pod Security Admission(PSA)은 별도 설치가 없고 네임스페이스 라벨만 붙이면 되기 때문에 겁먹을 필요는 없다.
결론부터 말하자면, PSA는 쿠버네티스 v1.25에 내장돼 GA(정식) 상태로 들어온 어드미션 컨트롤러다. 네임스페이스에 pod-security.kubernetes.io/enforce: baseline 같은 라벨을 붙이면, 그 네임스페이스에 생성되는 파드를 세 가지 표준(privileged, baseline, restricted) 중 하나로 검사한다. PSP처럼 RBAC로 정책을 바인딩할 필요 없이 라벨 하나로 강제가 걸린다.
1. PSP는 왜 없어졌고 PSA는 무엇인가
PSP는 파드의 보안 설정(권한 상승, 호스트 네임스페이스 사용, 특권 컨테이너 등)을 어드미션 단계에서 거르던 리소스다. 문제는 RBAC로 정책을 사용자와 서비스어카운트에 일일이 바인딩하는 구조가 복잡했고, 누가 어떤 정책의 적용을 받는지 예측하기 어려웠다는 점이다. 쿠버네티스는 PSP를 v1.21에서 사용 중단(deprecated) 처리하고 v1.25에서 완전히 제거했다.
대체재인 PSA는 Pod Security Standards라는 세 단계 프로필을 네임스페이스 단위로 적용하는 내장 어드미션 컨트롤러다. 별도 컨트롤러를 배포하지 않아도 되고, 정책은 라벨로 선언한다. 세 프로필의 성격은 이렇다.
- privileged: 아무 제약이 없다. 시스템 워크로드나 CNI, 스토리지 드라이버처럼 호스트 권한이 필요한 신뢰된 워크로드용이다.
- baseline: 알려진 권한 상승을 막는 최소한의 제약이다. 특권 컨테이너, 호스트 네임스페이스(
hostNetwork,hostPID,hostIPC),hostPath볼륨, 안전 목록 밖의 리눅스 capability 추가 등을 금지한다. - restricted: 하드닝 모범 사례까지 강제한다. baseline에 더해
runAsNonRoot: true,allowPrivilegeEscalation: false, 모든 capabilitydrop: ["ALL"],seccompProfile를RuntimeDefault또는Localhost로 지정하도록 요구한다.
2. 세 가지 모드와 라벨 형식
PSA는 프로필을 세 가지 모드로 적용한다. 이 부분을 이해하는 게 마이그레이션의 핵심이다.
- enforce: 위반한 파드를 거부한다. 파드가 아예 생성되지 않는다.
- audit: 파드는 허용하되 감사 로그(audit log)에 위반 내용을 주석으로 남긴다.
- warn: 파드는 허용하되
kubectl apply실행자에게 경고 메시지를 즉시 보여준다.
라벨 키 형식은 pod-security.kubernetes.io/<MODE>: <LEVEL>이다. 여기에 정책 버전을 고정하는 선택 라벨 pod-security.kubernetes.io/<MODE>-version: <VERSION>을 붙일 수 있다. 버전을 고정해두면 클러스터를 올렸을 때 표준이 조용히 강화돼 기존 파드가 갑자기 거부되는 사고를 막는다.
apiVersion: v1
kind: Namespace
metadata:
name: payments
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: v1.30
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
위 예시는 실무에서 자주 쓰는 조합이다. enforce는 baseline으로 걸어 최소 방어선을 강제하면서, audit와 warn은 한 단계 높은 restricted로 걸어둔다. 이렇게 하면 실제 거부는 baseline 기준으로만 일어나고, restricted로 올렸을 때 뭐가 걸릴지는 로그와 경고로 미리 확인할 수 있다.
3. 라벨을 붙이고 동작 확인하기
이미 존재하는 네임스페이스라면 kubectl label로 바로 붙인다.
$ kubectl label namespace payments \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/warn=restricted
namespace/payments labeled
이 상태에서 특권 컨테이너를 하나 던져보면 어떻게 막히는지 바로 보인다. 아래는 privileged: true를 준 파드다.
$ kubectl -n payments run bad --image=nginx --privileged Error from server (Forbidden): pods "bad" is forbidden: violates PodSecurity "baseline:latest": privileged (container "bad" must not set securityContext.privileged=true)
enforce가 baseline이라 특권 파드가 그 자리에서 거부된다. 여기서 한 번 헷갈리기 쉬운 부분이 있다. warn을 restricted로 걸어놨기 때문에, baseline은 통과하지만 restricted에는 못 미치는 평범한 파드를 올리면 거부는 안 되고 경고만 뜬다.
$ kubectl -n payments run web --image=nginx Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "web" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "web" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true, seccompProfile pod/web created
파드는 만들어졌다(pod/web created). restricted로 올리기 전에 어떤 필드를 고쳐야 하는지 이 경고가 그대로 알려준다. 마이그레이션할 때 이 warn 출력이 사실상 체크리스트 역할을 한다.
4. audit 모드로 오탐 먼저 걷어내기
운영 중인 네임스페이스에 곧바로 enforce: restricted를 걸면 기존 파드가 무더기로 거부되면서 배포가 멈추는 사고가 난다. 그래서 순서가 중요하다. 처음엔 enforce를 건드리지 말고 audit와 warn만 목표 수준으로 올려서 위반을 관찰하는 방식을 쓴다.
$ kubectl label --overwrite namespace payments \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
namespace/payments labeled
audit 모드로 걸린 위반은 API 서버의 감사 로그에 주석으로 남는다. 감사 로깅이 켜진 클러스터라면 아래처럼 pod-security.kubernetes.io/audit-violations 주석을 뽑아 어떤 워크로드가 걸리는지 집계할 수 있다.
$ grep audit-violations /var/log/kubernetes/audit.log \
| jq -r '.annotations["pod-security.kubernetes.io/audit-violations"]' \
| sort | uniq -c
매니지드 클러스터(EKS, GKE 등)라면 감사 로그가 클라우드 로깅으로 빠지므로 해당 로그 콘솔에서 같은 주석을 검색하면 된다. 며칠 관찰해서 위반 목록이 손볼 만한 수준으로 줄면, 그때 enforce를 올린다. warn은 실시간으로 배포자에게 보이므로 개발자가 스스로 매니페스트를 고치게 유도하는 데 유용하다.
5. 위반 워크로드를 restricted에 맞추기
restricted 위반의 대부분은 securityContext가 비어 있어서 난다. 앞의 warn 출력이 지적한 네 가지(권한 상승, capability, 비루트 실행, seccomp)를 채우면 대개 통과한다. 아래가 restricted를 만족하는 최소 형태다.
apiVersion: v1
kind: Pod
metadata:
name: web
namespace: payments
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: nginx:1.27-alpine
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
여기서 자주 걸리는 함정이 하나 있다. 표준 nginx 이미지는 80번 포트를 열려고 root로 동작하기 때문에 runAsNonRoot: true를 주면 컨테이너가 기동에 실패한다. 이럴 때는 비루트로 뜨도록 만들어진 이미지(nginxinc/nginx-unprivileged 같은)로 바꾸거나, 1024 이상 포트를 쓰도록 설정을 고쳐야 한다. 즉 restricted는 매니페스트만이 아니라 이미지 자체가 비루트로 돌 수 있어야 충족된다.
참고로 파드 레벨과 컨테이너 레벨 securityContext 중 어디에 어떤 필드를 넣어야 하는지 헷갈릴 수 있는데, runAsNonRoot와 seccompProfile은 파드 레벨에 한 번 주면 컨테이너가 상속하고, allowPrivilegeEscalation과 capabilities는 컨테이너 레벨에 지정한다.
6. 예외 처리와 클러스터 기본값
CNI 에이전트나 로그 수집기처럼 정말로 특권이 필요한 워크로드가 있다. 이런 네임스페이스는 pod-security.kubernetes.io/enforce: privileged로 열어두거나, 아예 PSA 검사에서 제외한다. 제외(exemption)는 라벨이 아니라 어드미션 컨트롤러 설정 파일(AdmissionConfiguration)에서 네임스페이스, 사용자명, RuntimeClass 세 가지 기준으로 지정한다.
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1
kind: PodSecurityConfiguration
defaults:
enforce: baseline
enforce-version: latest
audit: restricted
warn: restricted
exemptions:
namespaces: [kube-system]
runtimeClasses: []
usernames: []
defaults 블록은 라벨이 없는 네임스페이스에 적용할 클러스터 전역 기본값이다. 이걸 baseline으로 깔아두면 라벨을 깜빡한 네임스페이스도 최소 방어선 안에 들어온다. 다만 이 설정은 API 서버 기동 플래그(--admission-control-config-file)로 물려야 하므로, 컨트롤 플레인에 손댈 수 없는 매니지드 클러스터에서는 쓰기 어렵다. 그런 환경에서는 네임스페이스 라벨로만 관리하고, 신규 네임스페이스 생성 시 라벨을 강제하는 정책 도구(Kyverno 등)를 별도로 붙이는 방식을 권한다.
주의할 점 하나. 제외에서 kube-system의 컨트롤러 서비스어카운트를 열어주면, 그 컨트롤러가 만드는 워크로드 리소스(디플로이먼트가 만드는 레플리카셋의 파드 등)도 함께 검사를 우회한다. 예외 범위는 최소한으로 잡는 게 안전하다.
7. 마이그레이션 순서 정리
PSP에서 PSA로 넘어갈 때 실무에서 쓰는 순서는 이렇다. 먼저 모든 대상 네임스페이스에 warn과 audit을 목표 프로필로 걸어 위반을 수집한다. 다음으로 warn 출력과 audit 주석을 보고 워크로드 매니페스트와 이미지를 고친다. 위반이 정리되면 enforce를 baseline으로 먼저 올려 특권 파드부터 차단하고, 안정되면 필요한 네임스페이스만 enforce를 restricted로 승격한다. 마지막으로 enforce에는 반드시 -version 라벨로 버전을 고정해 이후 업그레이드에서 정책이 조용히 바뀌는 걸 막는다.
PSA는 PSP만큼 세밀하지는 않다. 특정 hostPath 경로만 허용하거나 이미지 레지스트리를 제한하는 것처럼 세 표준 프로필로 표현되지 않는 정책이 필요하면 Kyverno나 OPA Gatekeeper 같은 정책 엔진을 함께 쓴다. 반대로 표준 프로필로 충분한 대부분의 클러스터라면, 별도 설치 없이 라벨만으로 끝나는 PSA가 가장 손이 덜 간다.