有创建 Pod 的权限,但特权 Pod 必须被拦下
한국어 원문으로 표시합니다.
목표
ValidatingAdmissionPolicy(VAP)로 "특권 컨테이너 금지" 와 "hostPath 볼륨 금지" 정책을 CEL 로 직접 쓰고, 바인딩으로 한 네임스페이스에만 켠 뒤, API 서버가 돌려주는 거절 메시지와 정상 파드의 승인을 양쪽 다 관찰합니다.
왜 중요한가
쿠버네티스의 모든 쓰기 요청은 인증 → 인가(RBAC) → 어드미션 을 거쳐야 etcd 에 저장됩니다. RBAC 은
"이 사용자가 파드를 만들어도 되는가" 까지만 답하고, "그 파드의 내용이 안전한가" 는 묻지 않습니다.
파드를 만들 권한만 있으면 privileged: true 나 노드의 / 를 hostPath 로 마운트한 파드로 노드를 통째로
장악할 수 있습니다. 그 구멍을 막는 층이 어드미션 제어입니다.
Pod Security Admission(PSA)은 네임스페이스 라벨 하나로 정해진 기준(privileged/baseline/restricted)을 켭니다. 편하지만 조직만의 규칙(허용 레지스트리, 특정 라벨 필수 등)은 표현할 수 없습니다. VAP 는 API 서버 안에서 CEL 식을 직접 평가하므로 웹훅 서버 없이도 원하는 규칙을 쓸 수 있고, 정책(무엇을 검사하나) 과 바인딩(어디에, Deny/Warn/Audit 중 무엇으로) 이 분리돼 있어 같은 정책을 네임스페이스마다 다르게 켤 수 있습니다.
거절 메시지에는 어떤 정책과 어떤 바인딩이 막았는지가 이름으로 찍힙니다. 사고 대응 때 "왜 배포가 안 되나" 를 가장 빨리 푸는 단서가 바로 이 한 줄입니다. 또 어드미션은 생성 요청 시점에만 일어나므로, 정책을 켜기 전에 이미 만들어진 오브젝트는 그대로 남는다는 점도 기억해 두세요.
단계
- 네임스페이스
kcsa-adm을 만들고 자동 라벨을 확인합니다. - 특권 컨테이너를 거절하는 정책
deny-privileged를 CEL 로 씁니다. - 바인딩
deny-privileged-binding으로kcsa-adm에만 Deny 로 켭니다. - 특권 파드를 만들어 보고 거절 메시지를
/root/kcsa-adm/denied-privileged.txt에 저장합니다. - 평범한 파드
good이 승인되는 것을 확인합니다. - hostPath 볼륨을 거절하는 정책
deny-hostpath와 바인딩을 만듭니다. - hostPath 파드를 만들어 보고 거절 메시지를
/root/kcsa-adm/denied-hostpath.txt에 저장합니다. - 무엇이 막히고 무엇이 통과했는지
/root/kcsa-adm/report.txt에 남깁니다.
참고
- 정책만 만들고 바인딩을 빠뜨리면 아무것도 막히지 않습니다.
kubectl get validatingadmissionpolicybinding으로 둘 다 있는지 먼저 보세요. validationActions를Warn으로 두면 요청은 통과하고 경고만 뜹니다. 막으려면Deny여야 합니다.- 이 실습의 파드는 컨테이너 없이 흉내 내지만, 어드미션은 API 요청 단계에서 일어나므로 거절·승인은 진짜입니다.
- 공식 문서: Validating Admission Policy · Pod Security Standards.
정책을 걸 격리 구역 만들기
네임스페이스 kcsa-adm 를 만듭니다. 이 네임스페이스에는 API 서버가 자동으로 kubernetes.io/metadata.name=kcsa-adm 라벨을 붙이며, 뒤 단계의 바인딩이 이 라벨로 대상을 고릅니다.
쿠버네티스는 모든 네임스페이스에 자기 이름을 값으로 하는 kubernetes.io/metadata.name 라벨을 자동으로 붙입니다. 만든 뒤 kubectl get ns <이름> --show-labels 로 확인해 보세요. create 를 --dry-run=client 로 만들어 apply 하면 여러 번 돌려도 안전합니다.
특권 컨테이너를 막는 규칙을 CEL 로 쓰다
ValidatingAdmissionPolicy deny-privileged 를 만듭니다. 파드 CREATE 요청에서 securityContext.privileged 가 true 인 컨테이너가 하나라도 있으면 거절하는 CEL 식을 쓰고, 거절 메시지는 privileged containers are not allowed 로 둡니다. failurePolicy 는 Fail.
정책은 matchConstraints.resourceRules 로 어떤 요청(코어 그룹 v1 pods, CREATE)을 볼지 정하고, validations[].expression 이 참이어야 통과합니다. object.spec.containers.all(c, ...) 처럼 모든 컨테이너가 조건을 만족하는지 묻되, securityContext 나 privileged 필드가 아예 없는 경우도 통과로 쳐야 하니 has() 로 먼저 확인하세요. 정책만으로는 아무것도 막히지 않습니다.
규칙을 kcsa-adm 에 묶어 실제로 켜기
ValidatingAdmissionPolicyBinding deny-privileged-binding 을 만듭니다. policyName 은 deny-privileged, validationActions 는 ["Deny"], 대상은 matchResources.namespaceSelector 의 matchLabels 로 kubernetes.io/metadata.name: kcsa-adm 인 네임스페이스만 고릅니다.
VAP 는 정책(무엇을 검사하나)과 바인딩(어디에, 어떤 행동으로)이 나뉘어 있습니다. 바인딩이 없으면 정책은 평가조차 되지 않습니다. validationActions 에는 Deny 외에 Warn·Audit 도 있는데, Deny 여야 요청이 실제로 거절됩니다. 네임스페이스를 고를 땐 1단계에서 확인한 자동 라벨을 쓰세요.
특권 파드가 문 앞에서 돌려보내지다
네임스페이스 kcsa-adm 에 securityContext.privileged: true 인 컨테이너를 가진 파드(이미지 nginx:1.27-alpine)를 만들어 보고, API 서버가 돌려준 거절 메시지 전체를 /root/kcsa-adm/denied-privileged.txt 에 저장합니다.
거절은 파드가 생성되기 전에 일어나므로 kubectl 이 0 이 아닌 종료 코드와 함께 오류를 표준 에러로 출력합니다. 표준 에러까지 파일로 받아야 합니다(2>&1). 메시지에는 어떤 정책과 어떤 바인딩이 거절했는지가 이름으로 들어 있습니다. 바인딩 직후엔 반영에 1~2초 걸릴 수 있으니, 파드가 만들어져 버렸다면 지우고 다시 시도하세요.
평범한 파드는 막힘 없이 들어간다
네임스페이스 kcsa-adm 에 특권 설정이 없는 평범한 파드 good(이미지 nginx:1.27-alpine)을 만듭니다. 정책이 켜져 있어도 이 파드는 승인돼야 합니다.
좋은 정책은 나쁜 요청만 막고 정상 요청은 그대로 통과시킵니다. CEL 식에서 securityContext 가 없는 컨테이너를 통과로 처리했는지가 여기서 드러납니다. 만들어졌다면 kubectl get pod good -n kcsa-adm 로 보이고, 막혔다면 4단계와 같은 모양의 오류가 납니다.
노드 디스크로 통하는 hostPath 도 막기
ValidatingAdmissionPolicy deny-hostpath 와 바인딩 deny-hostpath-binding 을 만듭니다. 정책은 파드 CREATE 에서 hostPath 볼륨이 하나라도 있으면 거절하고(메시지 hostPath volumes are not allowed, failurePolicy: Fail), 바인딩은 validationActions ["Deny"] 로 kubernetes.io/metadata.name: kcsa-adm 네임스페이스에만 적용합니다.
구조는 2·3단계와 같고 검사 대상만 volumes 로 바뀝니다. volumes 필드가 아예 없는 파드도 있으니 has(object.spec.volumes) 로 먼저 확인하고, 각 볼륨에 hostPath 필드가 있는지를 has() 로 물으세요. 정책과 바인딩을 --- 로 이어 한 번에 apply 해도 됩니다.
노드의 /etc 를 마운트하려던 파드가 거절되다
네임스페이스 kcsa-adm 에 노드의 /etc 를 hostPath 볼륨으로 마운트하는 파드(이미지 nginx:1.27-alpine)를 만들어 보고, 거절 메시지 전체를 /root/kcsa-adm/denied-hostpath.txt 에 저장합니다.
파드 spec 의 volumes 에 hostPath 볼륨을 두고 컨테이너 volumeMounts 로 붙이는 모양입니다. 4단계처럼 표준 에러를 파일로 받으세요. 이번 메시지에 찍히는 정책·바인딩 이름이 4단계와 다르다는 점을 확인하세요.
무엇이 막히고 무엇이 통과했는지 장부로 남기기
관찰 결과를 /root/kcsa-adm/report.txt 에 네 줄로 적습니다 — privileged=denied, hostpath=denied, plain=allowed, enforcement=validatingadmissionpolicy. 채점기는 각 줄을 실제 클러스터에 파드를 만들어 보며 대조합니다.
값은 denied 또는 allowed 둘 중 하나이고, 마지막 줄은 이 거절을 누가 집행했는지(PSA 라벨이 아니라 어떤 어드미션 메커니즘인지)를 소문자 한 낱말로 적습니다. 추측이 아니라 4·5·7단계에서 직접 본 결과를 옮기세요.