정책을 코드로 · 파드 보안 표준을 라벨 하나로 켠다 · 실습
경고만 켜 둔 채 enforce 로 올렸더니 배포가 멈췄다
목표
쿠버네티스에 내장된 파드 보안 어드미션(PSA)을 네임스페이스 라벨만으로 운용합니다. 관측(warn·audit)에서 차단(enforce)으로 올리는 순서를 직접 밟고, 올리기 전에 무엇이 막힐지 미리 뽑아 보고서로 만듭니다.
왜 중요한가
PSA 는 설치할 것이 없습니다 — API 서버에 이미 켜져 있고, 네임스페이스에 라벨 세 개를 거는 것이 전부입니다. 그래서 쉬워 보이지만, 사고는 언제나 순서를 건너뛸 때 납니다. enforce 를 바로 거는 팀은 그날 배포가 전부 막히고, warn 만 켜 둔 팀은 경고를 아무도 읽지 않아 반년 뒤 같은 자리에 섭니다. 두 실패 사이에 있는 것이 이 실습이 다루는 순서입니다 — 관측으로 현황을 모으고, 수준의 판을 고정해 클러스터 업그레이드와 정책 변경을 떼어 놓고, 전환 직전에 기존 파드 위반을 미리 뽑아 고칠 목록을 만든 뒤에 올립니다. 그리고 올린 뒤에도 이미 떠 있는 파드는 막지 못한다는 한계를 알아야, 라벨을 바꾼 날을 완료로 착각하지 않습니다.
단계
1. /root/psa/plain.yaml 에 파드 하나를 적으세요 — 이름 web, 라벨 app: web, 컨테이너 이름 web, 이미지 nginx:1.27 이고 securityContext 는 한 줄도 넣지 않습니다. 네임스페이스 psa-legacy 를 PSA 라벨 없이 만들고 이 파드를 적용하세요. 그리고 /root/psa/default.txt 에 라벨이 하나도 없는 네임스페이스에 적용되는 enforce 수준의 이름을 한 낱말로 적으세요.
2. 네임스페이스 psa-observe 를 만들고 pod-security.kubernetes.io/warn=restricted 와 pod-security.kubernetes.io/audit=restricted 두 라벨만 거세요(enforce 는 걸지 않습니다). 같은 plain.yaml 을 kubectl create -f plain.yaml -n psa-observe 로 넣고, 표준 출력과 표준 오류를 함께 /root/psa/warn.txt 에 저장하세요. 파드가 만들어졌다는 줄과 네 가지 위반 이유가 모두 그 파일에 있어야 합니다.
3. 네임스페이스 psa-strict 를 만들고 pod-security.kubernetes.io/enforce=restricted 를 거세요. 같은 plain.yaml 을 kubectl create -f plain.yaml -n psa-strict --dry-run=server 로 넣어 거부 메시지를 표준 오류까지 /root/psa/denied.txt 에 저장하세요. 그 메시지에 적힌 네 가지를 모두 고친 파드를 /root/psa/hardened.yaml 에 쓰고(이름 web-hardened, 이미지는 그대로 nginx:1.27) psa-strict 에 실제로 적용하세요.
4. 네임스페이스 psa-baseline 을 만들고 pod-security.kubernetes.io/enforce=baseline 을 거세요. /root/psa/privileged.yaml 에 baseline 도 어기는 파드를 쓰세요(이름 breaker, 컨테이너 이름 breaker, 이미지 busybox:1.36). 그리고 /root/psa/gap.txt 에 세 매니페스트(plain.yaml·hardened.yaml·privileged.yaml)를 두 네임스페이스에 각각 넣어 본 결과를 <파일이름> baseline=<allow|deny> restricted=<allow|deny> 꼴로 한 줄씩 적으세요. 판정은 --dry-run=server 로 하고 파드를 실제로 만들지 않습니다.
5. 세 네임스페이스의 수준 라벨마다 짝이 되는 버전 라벨을 v1.30 으로 거세요 — psa-strict 와 psa-baseline 은 pod-security.kubernetes.io/enforce-version, psa-observe 는 pod-security.kubernetes.io/warn-version 과 pod-security.kubernetes.io/audit-version 입니다. 그리고 /root/psa/pinned.sh 를 만드세요 — 인자 없이 실행하면 enforce·audit·warn 라벨은 걸려 있는데 그 짝인 -version 라벨이 없는 네임스페이스의 이름만 한 줄씩(정렬해서) 출력합니다.
6. 먼저 psa-legacy 에 파드를 둘 더 넣으세요 — plain.yaml 의 이름만 api 로 바꾼 것과 hardened.yaml 의 이름만 batch 로 바꾼 것입니다. 그다음 판정 전용 네임스페이스 psa-canary 를 만들어 pod-security.kubernetes.io/enforce=restricted 와 pod-security.kubernetes.io/enforce-version=v1.30 을 거세요. kubectl label ns psa-legacy pod-security.kubernetes.io/enforce=restricted --overwrite --dry-run=server 의 출력을 표준 오류까지 /root/psa/preview.txt 에 저장하세요 — 라벨이 실제로 붙어서는 안 됩니다. 마지막으로 /root/psa/violators.sh <네임스페이스> 를 만드세요: 그 네임스페이스의 파드를 하나씩 psa-canary 로 다시 제출해 (--dry-run=server) restricted 를 어기는 것의 이름만 한 줄씩 정렬해 출력합니다. ./violators.sh psa-legacy 의 결과를 /root/psa/violators.txt 에 저장하세요.
7. psa-legacy 에 pod-security.kubernetes.io/enforce=restricted 와 pod-security.kubernetes.io/enforce-version=v1.30 을 이번에는 진짜로 거세요. 그다음 hardened.yaml 을 psa-legacy 에 적용하고(통과해야 합니다), plain.yaml 을 kubectl create -n psa-legacy --dry-run=server 로 넣어 거부 메시지를 표준 오류까지 /root/psa/enforced.txt 에 저장한 뒤 kubectl get pod -n psa-legacy 의 출력을 그 파일 뒤에 이어 붙이세요. 이미 떠 있던 web 과 api 는 지우지 않습니다.
8. /root/psa/targets.txt 에 검사할 네임스페이스 이름을 한 줄씩 적으세요 — psa-legacy·psa-observe·psa-strict·psa-baseline 넷입니다. /root/psa/readiness.sh [목록파일] 을 만드세요(인자를 빼면 /root/psa/targets.txt 를 씁니다). 목록의 네임스페이스마다 한 줄씩 판정을 출력합니다: 이미 enforce=restricted 면 <이름> ENFORCED, 어기는 파드가 하나도 없으면 <이름> READY, 있으면 <이름> BLOCKED <개수>, 그런 네임스페이스가 없으면 <이름> MISSING 입니다. 라벨을 실제로 바꾸어서는 안 됩니다. 실행 결과를 /root/psa/report.txt 에 저장하세요.
참고
- 실습 파드 안에는 kwok 이 띄운 진짜 kube-apiserver v1.30.4 가 있습니다. 먼저
export KUBECONFIG=/root/.kube/config를 하세요. 컨테이너가 실제로 실행되지는 않지만 어드미션은 진짜로 동작합니다. - 라벨 형식:
pod-security.kubernetes.io/<enforce|audit|warn>=<privileged|baseline|restricted>와 짝이 되는pod-security.kubernetes.io/<모드>-version=<latest|v1.x>. - 어드미션만 태우고 오브젝트는 남기지 않으려면
kubectl create -f - --dry-run=server를 씁니다.kubectl label ns ... --dry-run=server는 그 네임스페이스의 기존 파드 위반을 경고로 미리 알려 줍니다. - 흔한 실수: 경고가 표준 오류로 나오는 것을 모르고
> 파일만 걸어 빈 파일을 남기는 것. - 흔한 실수: 같은 이름의 파드가 이미 있어 어드미션까지 가지 못하고 AlreadyExists 로 끝나는 것 — 판정용 제출은 이름을 바꿔 보냅니다.
- 흔한 실수: 라벨을 올린 날 아무 일도 안 일어나는 것을 보고 안전하다고 판단하는 것. 기존 파드는 재배포될 때 막힙니다.
- [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) · [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) · [Enforce Pod Security Standards with Namespace Labels](https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/) · [Admission Controllers Reference](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/) · [Migrate from PodSecurityPolicy to PSA](https://kubernetes.io/docs/tasks/configure-pod-container/migrate-from-psp/)
단계 8개
- 라벨이 하나도 없는 네임스페이스는 아무것도 막지 않는다
- 경고만 켰더니 같은 파드가 경고와 함께 만들어졌다
- 네 가지 이유를 읽고 파드를 restricted 에 맞게 고친다
- baseline 은 통과시키고 restricted 만 막는 파드
- latest 로 둔 네임스페이스가 클러스터를 올리는 날 멈춘다
- 올리기 전에 무엇이 막힐지 미리 뽑는다
- 올린 뒤에도 이미 떠 있던 파드는 그대로 돈다
- 어느 네임스페이스부터 올려도 되는지 보고서로 만든다