KCSA — 쿠버네티스 보안 어소시에이트 · 플랫폼 보안 · 이론
PSP 는 왜 죽었고 PSA 는 무엇이 다른가
한 줄 요약
PSP 는 "정책과 사용자를 RBAC 으로 연결하는" 구조였고, 그게 너무 어려웠습니다.
PSA 는 그 연결을 버리고 네임스페이스 라벨 세 개로 단순화했습니다. 표현력을 잃는 대신
운영 가능성을 얻은 교환입니다.
왜 이게 필요했나
PodSecurityPolicy 는 이렇게 동작했습니다 — PSP 오브젝트를 만들고, ClusterRole 에 그 PSP 에 대한use 권한을 넣고, 그것을 파드를 만드는 주체(사람이 아니라 파드를 만드는 컨트롤러의 SA)에
바인딩합니다. 그러면 어드미션 시점에 "이 주체가 쓸 수 있는 PSP 중 이 파드를 허용하는 게 있나"를 찾습니다.
문제가 여럿이었습니다.
- 여러 PSP 가 매칭되면 어느 것이 적용됐는지 알기 어려웠습니다. 정렬 규칙이 있긴 했지만 직관적이지 않았습니다.
- PSP 는 mutating 이기도 해서 파드 스펙을 조용히 바꿨습니다. 내가 쓴 것과 실행되는 것이 달랐습니다.
- 요청 주체가 사람이 아니라 컨트롤러 SA 라는 점이 계속 혼란을 낳았습니다.
- 그래서 디버깅이 지옥이었습니다. "왜 이 파드가 거부됐나"에 답하는 데 시간이 오래 걸렸습니다.
결과적으로 많은 클러스터가 PSP 를 아예 켜지 않거나, 모두에게 permissive PSP 를 주고 끝냈습니다.
쓰이지 않는 보안 기능은 보안이 아닙니다. PSP 는 v1.25 에서 완전히 제거됐습니다.
어떻게 동작하나
세 개의 프로파일
| 프로파일 | 성격 | 대표 제한 |
| --- | --- | --- |
| privileged | 무제한 | 없음. CNI·스토리지 드라이버 등 시스템 워크로드용 |
| baseline | 알려진 권한 상승만 차단 | hostNetwork/hostPID/hostIPC 금지, privileged 금지, hostPath 금지, 위험한 capability 추가 금지 |
| restricted | 모범 사례 강제 | baseline + runAsNonRoot 필수, seccompProfile 필수, capabilities 전부 drop, allowPrivilegeEscalation=false |
restricted 를 만족시키려면 파드 스펙에 최소 네 가지가 필요합니다 —runAsNonRoot: true, allowPrivilegeEscalation: false, capabilities.drop: [ALL],seccompProfile.type: RuntimeDefault.
세 개의 모드
| 모드 | 동작 | 쓰는 때 |
| --- | --- | --- |
| enforce | 위반 파드 생성 거부 | 실제 적용 |
| audit | 감사 로그에만 기록, 생성은 허용 | 영향 파악 |
| warn | kubectl 사용자에게 경고 표시, 생성은 허용 | 마이그레이션 준비 |
라벨은 pod-security.kubernetes.io/<모드>: <프로파일> 형태이고,<모드>-version 으로 버전을 고정할 수 있습니다.
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버전 고정이 왜 중요한가. 고정하지 않으면 latest 가 되어, 클러스터를 업그레이드하는 순간
정책 내용이 바뀔 수 있습니다. 배포한 적 없는데 파드가 거부되기 시작하는 사고가 여기서 나옵니다.
위 예시는 실무에서 가장 흔한 조합입니다 — **enforce 는 baseline 으로 실제 차단하되,
audit/warn 은 restricted 로 걸어 "restricted 로 올리면 무엇이 깨지는지"를 미리 수집**합니다.
반드시 알아야 할 성질: 어드미션 시점에만 동작한다
PSA 는 파드가 만들어질 때만 판단합니다. 이미 돌고 있는 파드는 라벨을 올려도 쫓겨나지 않습니다.
그래서 enforce 를 restricted 로 올린 직후에는 아무 일도 안 일어난 것처럼 보이다가,
다음 롤아웃이나 노드 교체 때 파드가 안 뜨면서 터집니다. 정책 변경과 사고 사이에 시차가 있습니다.
면제(exemptions)
네임스페이스 라벨로는 표현할 수 없는 예외가 있어서, AdmissionConfiguration 에 세 축의 면제를 둡니다.
usernames— 특정 사용자의 요청은 검사 안 함runtimeClasses— 특정 RuntimeClass 를 쓰는 파드는 검사 안 함namespaces— 특정 네임스페이스는 검사 안 함 (보통kube-system)
면제는 클러스터 전체 설정이라 apiserver 를 다시 시작해야 하고, 네임스페이스 라벨로는 못 바꿉니다.
이게 안전장치이기도 합니다 — 네임스페이스 관리자가 스스로를 면제할 수 없으니까요.
PSA 로 부족할 때
PSA 는 프로파일이 셋뿐이라 "이 레지스트리의 이미지만 허용", "모든 파드에 리소스 제한 필수",
"팀 라벨 없는 네임스페이스 금지" 같은 건 표현할 수 없습니다. 그때 정책 엔진을 얹습니다.
- Kyverno — 학습 곡선 낮음, YAML 로 정책 작성, 리소스 생성(generate)까지 가능, CNCF Incubating
- OPA Gatekeeper — Rego 언어, 표현력 매우 높음, 교차 리소스 검증 가능, 리소스 소비 높음, CNCF Graduated
저자의 블로그가 내린 판단은 명확합니다 — **"단순한 정책에는 Kyverno, 복잡한 교차 리소스 검증에는
OPA Gatekeeper", 그리고 권장 조합은 RBAC(기본 인가) + PSA(파드 보안 기준) + 정책 엔진(커스텀)** 입니다.
Gatekeeper 에는 어드미션 웹훅이 놓치는 구멍이 하나 있어서 Audit Controller 가 따로 있습니다.
웹훅은 신규 요청만 막으므로, 정책을 나중에 추가하면 이미 배포된 위반 리소스는 그대로 남습니다.
Audit 이 주기적으로 전체를 훑어 그것을 찾아냅니다. enforcementAction: dryrun 으로 차단 없이
위반만 수집하는 것이 새 정책의 영향 평가 표준 절차입니다.
현장에서 만나는 모습
저자의 홈랩 재구축 기록에 PSA 와 정확히 같은 원리의 사건이 있습니다.
Cilium 설치 직후 hubble-relay 와 hubble-ui 가 Pending 이었고 이유는0/1 nodes are available: 1 node(s) had untolerated taint(s) 였습니다.
컨트롤 플레인의 NoSchedule taint 를 Deployment 가 톨러레이트하지 않았던 것인데,
"정상 동작인데 사고처럼 보이는" 유형이라는 점에서 PSA 마이그레이션과 성격이 같습니다.
저자의 결론도 같았습니다 — "이건 오류가 아니라 정상 동작입니다."
PSA 마이그레이션의 표준 절차도 저자가 정리해 둔 그대로입니다 —
① dry-run 으로 현재 상태 감사(kubectl label --dry-run=server --overwrite ns --all ...),
② warn/audit 먼저 적용, ③ 위반 워크로드 수정, ④ enforce 활성화,
⑤ AdmissionConfiguration 으로 신규 네임스페이스에 기본값 적용.
"점진적 접근이 핵심이며, 절대 한 번에 enforce 를 적용하지 않습니다."
이어서 읽을 것
먼저 시크릿 관리를 한 편 더 읽고, 그다음 실습에서 네임스페이스에 PSA 라벨을 직접 붙입니다.
restricted 위반 파드가 거부되는 것과 그 거부 메시지를 확인하고, restricted 를 통과하는 파드를
만들어 보고, 마지막으로 **라벨만 바꿔 등급을 올렸을 때 기존 파드는 그대로 살아 있고
새 파드만 거부되는 것**을 직접 관찰합니다.