LabHub

CKS — 쿠버네티스 보안 전문가 · 마이크로서비스 취약점 최소화 · 이론

라벨 세 줄이 PSP 를 대체한 이유

LabHub 에서 이어서 보기

한 줄 요약

Pod Security Admission 은 네임스페이스 라벨 몇 줄로 워크로드의 보안 하한선을 정합니다.
그리고 Secret 은 base64 로 인코딩될 뿐 암호화되지 않으므로, 별도의 통제가 필요합니다.

왜 이게 필요했나

PodSecurityPolicy 는 1.21 에서 deprecated 되고 1.25 에서 제거됐습니다. 이유는 두 가지였습니다.
PSP 를 쓰려면 워크로드의 ServiceAccount 에 "이 PSP 를 use 할 수 있다"는 RBAC 권한을 줘야 했는데,
그 구조 자체가 권한 상승 경로를 만들었습니다. 그리고 어떤 파드에 어떤 PSP 가 적용될지 예측하기가
매우 어려웠습니다. 여러 PSP 중 하나가 선택되는 규칙이 복잡했기 때문입니다.

대체재인 PSA 는 정반대로 단순합니다. 네임스페이스에 라벨을 붙이면 끝입니다.

pod-security.kubernetes.io/enforce: restrictedpod-security.kubernetes.io/enforce-version: v1.30pod-security.kubernetes.io/audit: restrictedpod-security.kubernetes.io/warn: restricted

enforce 는 위반 파드를 거부하고, audit 는 감사 로그에만 남기며, warn 은 kubectl 사용자에게
경고만 띄웁니다. 세 모드를 따로 설정할 수 있다는 것이 마이그레이션의 핵심입니다. 기존 클러스터에
곧바로 enforce: restricted 를 걸면 워크로드가 대량으로 막히므로, warnaudit 를 먼저
켜서 무엇이 걸리는지 관찰한 뒤 enforce 로 올립니다.

-version 라벨도 중요합니다. 프로파일의 정의는 쿠버네티스 버전마다 조금씩 강화됩니다.
버전을 못 박아 두면 클러스터 업그레이드가 곧 정책 강화로 이어져 파드가 갑자기 거부되는 일을
막을 수 있습니다.

어떻게 동작하나

프로파일은 세 단계입니다.

| 프로파일 | 핵심 제한 |
| --- | --- |
| privileged | 제한 없음. CNI·스토리지 드라이버 같은 시스템 워크로드용 |
| baseline | hostNetwork/hostPID/hostIPC 금지, privileged 금지, hostPath 금지, 위험 capability 금지 |
| restricted | baseline + runAsNonRoot 필수, seccompProfile 필수, capabilities drop ALL 필수, allowPrivilegeEscalation false 필수 |

restricted 를 통과하는 파드는 네 필드를 반드시 갖습니다. runAsNonRoot: true,
allowPrivilegeEscalation: false, capabilities.drop: ["ALL"],
seccompProfile.type: RuntimeDefault. 이 넷 중 하나만 빠져도 거부되고, 오류 메시지에
어떤 필드가 왜 걸렸는지 그대로 나옵니다.

두 번째 축은 Secret 입니다. 실무자가 가장 자주 오해하는 부분입니다. Secret 매니페스트의 값이
base64 로 되어 있어 암호화된 것처럼 보이지만, base64 는 인코딩이지 암호화가 아닙니다. 키가 없고
되돌리는 데 명령 하나면 됩니다. 기본 설정에서 Secret 은 etcd 에 평문으로 저장되므로, etcd 백업
파일이나 디스크 이미지를 얻은 사람은 모든 Secret 을 읽습니다. 저장 시 암호화는
EncryptionConfiguration 파일을 만들고 kube-apiserver 의 --encryption-provider-config
지정해 켭니다. 이때 identity 프로바이더는 반드시 목록의 마지막이어야 합니다. 앞에 오면 평문
저장으로 되돌아갑니다. 설정을 바꿔도 기존 Secret 은 다시 쓰기 전까지 평문으로 남으므로
전체를 한 번 갱신해야 합니다.

세 번째는 주입 방식입니다. 환경변수로 넣으면 /proc/<PID>/environ 으로 읽히고, 자식 프로세스에
상속되며, 크래시 리포트나 디버그 페이지에 통째로 실려 나갑니다. 볼륨 마운트가 더 안전하고,
defaultMode: 0400 으로 소유자 읽기만 허용하는 것이 관례입니다.

현장에서 만나는 모습

etcd 를 직접 열어 보면 오해가 사라집니다. 암호화를 켜지 않은 상태에서
etcdctl get /registry/secrets/default/app-db | hexdump -C 를 돌리면 키 경로와 값이 그대로
보입니다. 암호화를 켜면 같은 자리에 k8s:enc:kms:v2: 같은 프로바이더 접두사가 붙고 본문은
읽을 수 없게 됩니다. 이 차이를 눈으로 본 사람과 안 본 사람의 설계가 다릅니다.

RBAC 쪽 오해도 자주 봅니다. 어떤 네임스페이스에 get secrets 권한이 있는 사람은 그 네임스페이스의
모든 Secret 을 읽습니다. 개발자에게 편의로 준 편집 권한이 사실상 프로덕션 자격증명 열람 권한인
경우가 매우 흔합니다. kubectl auth can-i get secrets --namespace payments --as dev@example.com
한 줄로 확인할 수 있는데, 확인해 본 팀이 드뭅니다. Role 의 resourceNames 로 특정 Secret 만
허용하는 방식이 있다는 사실도 잘 알려져 있지 않습니다.

멀티 테넌시 관점에서도 같은 이야기가 반복됩니다. 네임스페이스 기반 격리는 API 서버를 공유하므로
CRD 설치 충돌과 노이지 네이버 문제가 남습니다. vCluster 처럼 테넌트마다 독립 API 서버를 주는
방식은 이 문제를 풀지만, 워크로드 파드는 결국 호스트 클러스터의 네임스페이스에
<vcluster>-x-<이름>-x-<네임스페이스> 형태로 실체화됩니다. 즉 호스트 쪽 PSS 와 NetworkPolicy
위생은 그대로 필요합니다. 추상화 계층이 하나 늘어난다고 아래층 통제가 면제되지 않습니다.

다음 실습에서 할 것

먼저 네임스페이스 두 개에 baseline 과 restricted 를 각각 걸고, restricted 를 통과하는 파드를
만든 뒤 위반 파드가 실제로 거부되는 것을 확인합니다. 그다음 Secret 실습에서 타입별 생성,
환경변수 주입과 볼륨 마운트의 차이, defaultMode, immutable Secret, resourceNames 로 좁힌
RBAC, 그리고 EncryptionConfiguration 파일 작성까지 다룹니다.