LabHub
배우기 러닝패스 코스

Policy as Code

We left it on warn-only, then switched to enforce and every deploy stopped

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

쿠버네티스에 내장된 파드 보안 어드미션(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=restrictedpod-security.kubernetes.io/audit=restricted 두 라벨만 거세요(enforce 는 걸지 않습니다). 같은 plain.yamlkubectl create -f plain.yaml -n psa-observe 로 넣고, 표준 출력과 표준 오류를 함께 /root/psa/warn.txt 에 저장하세요. 파드가 만들어졌다는 줄과 네 가지 위반 이유가 모두 그 파일에 있어야 합니다.
  3. 네임스페이스 psa-strict 를 만들고 pod-security.kubernetes.io/enforce=restricted 를 거세요. 같은 plain.yamlkubectl 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-strictpsa-baselinepod-security.kubernetes.io/enforce-version, psa-observepod-security.kubernetes.io/warn-versionpod-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=restrictedpod-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-legacypod-security.kubernetes.io/enforce=restrictedpod-security.kubernetes.io/enforce-version=v1.30 을 이번에는 진짜로 거세요. 그다음 hardened.yamlpsa-legacy 에 적용하고(통과해야 합니다), plain.yamlkubectl create -n psa-legacy --dry-run=server 로 넣어 거부 메시지를 표준 오류까지 /root/psa/enforced.txt 에 저장한 뒤 kubectl get pod -n psa-legacy 의 출력을 그 파일 뒤에 이어 붙이세요. 이미 떠 있던 webapi 는 지우지 않습니다.
  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 에 저장하세요.

참고

라벨이 하나도 없는 네임스페이스는 아무것도 막지 않는다

/root/psa/plain.yaml 에 파드 하나를 적으세요 — 이름 web, 라벨 app: web, 컨테이너 이름 web, 이미지 nginx:1.27 이고 securityContext 는 한 줄도 넣지 않습니다. 네임스페이스 psa-legacy 를 PSA 라벨 없이 만들고 이 파드를 적용하세요. 그리고 /root/psa/default.txt 에 라벨이 하나도 없는 네임스페이스에 적용되는 enforce 수준의 이름을 한 낱말로 적으세요.

PodSecurity 는 API 서버에 이미 켜져 있는 어드미션 컨트롤러이고, 어느 수준으로 볼지는 오직 네임스페이스 라벨이 정합니다. 라벨이 없으면 클러스터 기본값이 쓰이는데 그 기본값은 아무것도 막지 않는 쪽입니다. Pod Security Standards 에는 수준이 셋 있습니다 — 그중 가장 느슨한 것의 이름입니다.

경고만 켰더니 같은 파드가 경고와 함께 만들어졌다

네임스페이스 psa-observe 를 만들고 pod-security.kubernetes.io/warn=restrictedpod-security.kubernetes.io/audit=restricted 두 라벨만 거세요(enforce 는 걸지 않습니다). 같은 plain.yamlkubectl create -f plain.yaml -n psa-observe 로 넣고, 표준 출력과 표준 오류를 함께 /root/psa/warn.txt 에 저장하세요. 파드가 만들어졌다는 줄과 네 가지 위반 이유가 모두 그 파일에 있어야 합니다.

PSA 의 세 모드는 서로 독립입니다 — enforce 만 요청을 막고, warn 은 요청한 사람에게 경고를 돌려주며, audit 는 감사 로그에 주석을 남깁니다. 경고는 표준 오류로 나오니 > 파일 2>&1 처럼 둘 다 받아야 합니다. 이 단계의 값어치는 '아무것도 못 막는데 왜 켜는가' 에 있습니다.

네 가지 이유를 읽고 파드를 restricted 에 맞게 고친다

네임스페이스 psa-strict 를 만들고 pod-security.kubernetes.io/enforce=restricted 를 거세요. 같은 plain.yamlkubectl 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 에 실제로 적용하세요.

거부 메시지가 고칠 자리를 그대로 알려 줍니다 — 어느 것이 파드 수준 securityContext 이고 어느 것이 컨테이너 수준인지만 가려서 넣으면 됩니다. runAsNonRoot 를 참으로 두면 이미지가 root 로 돌지 않는다는 뜻이므로 실행할 UID 도 함께 정해 주는 편이 안전합니다. capabilities 는 모두 버린 뒤 필요한 것만 다시 더하는 순서입니다.

baseline 은 통과시키고 restricted 만 막는 파드

네임스페이스 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 로 하고 파드를 실제로 만들지 않습니다.

baseline 은 널리 알려진 권한 상승만 막고, restricted 는 거기에 강화 규칙을 더합니다. 어느 쪽에서도 못 쓰는 것은 호스트 네임스페이스와 특권 컨테이너입니다. 같은 이름의 파드가 이미 있으면 어드미션까지 가기 전에 AlreadyExists 로 끝나니, 제출할 때 이름과 네임스페이스를 바꿔 보내는 것이 안전합니다 (kubectl create -f 파일 --dry-run=client -o json 으로 뽑아 jq 로 고친 뒤 다시 넘기면 됩니다).

latest 로 둔 네임스페이스가 클러스터를 올리는 날 멈춘다

세 네임스페이스의 수준 라벨마다 짝이 되는 버전 라벨을 v1.30 으로 거세요 — psa-strictpsa-baselinepod-security.kubernetes.io/enforce-version, psa-observepod-security.kubernetes.io/warn-versionpod-security.kubernetes.io/audit-version 입니다. 그리고 /root/psa/pinned.sh 를 만드세요 — 인자 없이 실행하면 enforce·audit·warn 라벨은 걸려 있는데 그 짝인 -version 라벨이 없는 네임스페이스의 이름만 한 줄씩(정렬해서) 출력합니다.

버전 라벨을 안 걸면 그 자리는 latest 로 돕니다 — 수준의 '지금 판' 이라는 뜻이라, 클러스터를 올리면 규칙도 함께 올라가 어제까지 통과하던 파드가 오늘 막힙니다. 라벨은 kubectl get ns -o json.items[].metadata.labels 에 통째로 들어 있고, 라벨이 하나도 없는 네임스페이스에서는 그 자리가 아예 없습니다. 세 모드를 낱말로 돌리면서 짝 라벨의 유무를 보는 문제입니다.

올리기 전에 무엇이 막힐지 미리 뽑는다

먼저 psa-legacy 에 파드를 둘 더 넣으세요 — plain.yaml 의 이름만 api 로 바꾼 것과 hardened.yaml 의 이름만 batch 로 바꾼 것입니다. 그다음 판정 전용 네임스페이스 psa-canary 를 만들어 pod-security.kubernetes.io/enforce=restrictedpod-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 에 저장하세요.

예고 경고는 파드가 많으면 (and N other pods) 로 줄여 버립니다 — 이름을 다 알려면 파드마다 따로 판정해야 합니다. 이미 떠 있는 파드는 어드미션을 다시 타지 않으므로, 같은 spec 을 그 수준을 강제하는 네임스페이스로 다시 제출해 보는 것이 방법입니다. kubectl get pod <이름> -n <ns> -o json 에서 apiVersion·kind·spec 만 추리고 metadata 는 새로 지어 주면 됩니다.

올린 뒤에도 이미 떠 있던 파드는 그대로 돈다

psa-legacypod-security.kubernetes.io/enforce=restrictedpod-security.kubernetes.io/enforce-version=v1.30 을 이번에는 진짜로 거세요. 그다음 hardened.yamlpsa-legacy 에 적용하고(통과해야 합니다), plain.yamlkubectl create -n psa-legacy --dry-run=server 로 넣어 거부 메시지를 표준 오류까지 /root/psa/enforced.txt 에 저장한 뒤 kubectl get pod -n psa-legacy 의 출력을 그 파일 뒤에 이어 붙이세요. 이미 떠 있던 webapi 는 지우지 않습니다.

PSA 는 어드미션 컨트롤러라 요청이 들어올 때만 판단합니다 — 이미 저장된 오브젝트는 다시 보지 않으므로 어기는 파드가 떠 있어도 쫓아내지 않습니다. 그래서 enforce 로 올린 직후의 클러스터는 '새로 들어오는 것만 깨끗한' 상태이고, 기존 파드는 재배포될 때 비로소 막힙니다. 이 시차를 모르면 올린 날 아무 일도 안 일어나는 것을 보고 안전하다고 잘못 판단하게 됩니다.

어느 네임스페이스부터 올려도 되는지 보고서로 만든다

/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 에 저장하세요.

앞 단계의 violators.sh 를 그대로 불러 쓰면 개수는 줄 수만 세면 됩니다. 이미 restricted 인 곳을 먼저 걸러 내야 하는 이유는, 그런 네임스페이스에 같은 값으로 라벨을 다시 걸면 값이 바뀌지 않아 예고 경고 자체가 나오지 않기 때문입니다. 목록 파일을 인자로 받게 해 두면 같은 스크립트를 다른 묶음에도 쓸 수 있습니다.