Tech Artifacts/K8s Security

K8s Security - 04. admission-control

인증·인가 통과한 뒤에도 한 번 더 검문한다 — 어드미션 컨트롤

한 팀이 "모든 Pod는 반드시 자원 제한(limits)을 가져야 한다"는 정책을 원했다. RBAC는 "누가 만들 수 있나"만 다루지 "만들어지는 객체가 어떤 모양*이어야 하나"는 다루지 못한다. 그래서 그 팀은 Pod를 만들고 *나서 수동으로 검사했다 — 늦었다. 어드미션 컨트롤(Admission Control)이 이 빈 칸을 채운다 — 인가 통과 , 객체가 etcd에 저장되기 에 한 번 더 검문한다.

이 글이 푸는 것은: 어드미션 컨트롤이 인증·인가 에 무엇을 검사하는가, 그리고 ValidatingAdmissionPolicy(CEL 기반, 1.30 GA)가 정책 코드를 어떻게 바꿨는가다.

요청의 네 단계 — 어드미션은 세 번째

flowchart LR
    REQ["요청"] --> A1["1. 인증<br/>"]
    A1 --> A2["2. 인가/RBAC<br/>"]
    A2 --> ADM["3. 어드미션 컨트롤"]
    ADM --> ETCD["4. etcd 저장"]
    ADM -. "Mutating: 객체 수정" .-> ADM
    ADM -. "Validating: 거부/허용" .-> ADM

어드미션 컨트롤은 인가 통과한 요청이 etcd에 들어가기 전에 끼어든다. 두 종류:

  • Mutating(변형): 객체를 고칠 수 있다. 예: 모든 Pod에 기본 라벨/annotation 자동 추가, image tag를 항상 digest로.
  • Validating(검증): 객체를 검사만 하고 허용/거부. 고칠 순 없다. 예: limits 없는 Pod는 거부.

순서가 중요: Mutating이 먼저 돌아 객체를 고치고, 그 다음 Validating이 고쳐진 객체를 검사.

어드미션 컨트롤러의 두 종류 — 내장 vs 동적

  • 내장(built-in) 어드미션 컨트롤러: apiserver에 컴파일된 것. 예: NamespaceLifecycle(없는 namespace로 만들기 금지), ServiceAccount(SA 자동 마운트), DefaultStorageClass. --enable-admission-plugins로 켬.
  • 동적(dynamic) 어드미션 컨트롤러: ValidatingWebhookConfiguration/MutatingWebhookConfiguration — 외부 서버(webhook)에 HTTP로 질의. OPA Gatekeeper, Kyverno 등이 이 패턴.

동적 webhook이 확장성의 핵심 — 클러스터 운영자가 정책을 코드로 추가.

ValidatingAdmissionPolicy — 정책을 webhook 없이 (1.30 GA)

전통적 어드미션 정책은 외부 webhook 서버가 필요했다(OPA Gatekeeper). 운영 부담(별도 서버 가용성). *ValidatingAdmissionPolicy**(CEL — Common Expression Language 기반)는 이를 바꾼다 — 정책을 *Kubernetes 객체로 직접 쓰고, apiserver가 내장 평가.

# Kubernetes 1.36
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata: {name: require-limits}
spec:
  matchConstraints:
    resourceRules: [{apiGroups: [""], apiVersions: ["v1"], operations: ["CREATE"], resources: ["pods"]}]
  validations:
  - expression: "object.spec.containers.all(c, has(c.resources) && has(c.resources.limits))"
    message: "모든 컨테이너는 resources.limits가 필요합니다"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata: {name: require-limits-binding}
spec:
  policyName: require-limits
  validationActions: [Deny]      # 위반 시 거부

이점:

  • 별도 서버 없음 — apiserver가 CEL로 평가. webhook 가용성/지연 문제 없음.
  • YAML로 정책 — 코드 배포가 Kubernetes 객체 배포.
  • 1.30 GA — 프로덕션 안전.

CEL은 Rego(OPA)와 다른 가벼운 표현 언어. ValidatingAdmissionPolicy에 채택. OPA Gatekeeper와 비교해 "외부 의존 없이"라는 장점이 있으나, 표현력/생태계는 OPA가 더 넓음. 선택은 팀 숙련도/요구.

MutatingAdmissionPolicy (1.36) — 변형도 정책으로

ValidatingAdmissionPolicy가 검증(거부/허용)만 한다면, MutatingAdmissionPolicy변형도 CEL 기반 정책으로 가져온다 — 1.36에서 Stable. "모든 Pod에 기본 annotation 추가", "image 태그를 digest로 변환" 같은 Mutating webhook 작업을 webhook 서버 없이 apiserver 내장에서 처리한다.

왜 검증과 변형이 분리되나? 검증은 객체를 안 고치고 거부만 하지만, 변형은 객체를 수정한다 — 부작용이 있는 작업이다. 이 둘을 같은 정책 객체에 섞으면 "거부했는데 이미 고쳤다" 같은 문제가 생길 수 있다. 그래서 Kubernetes는 검증(VAP)과 변형(MutatingAdmissionPolicy)을 별도 객체로 분리하고, 체인 순서(Mutating 먼저 → Validating 나중)로 부작용을 제어한다.

정확한 1.36 MutatingAdmissionPolicy 상태와 지원 범위는 CHANGELOG 1.36와 KEP에서 실측.

직접 확인하기

# Kubernetes 1.36 — 켜진 어드미션 플러그인
kubectl get pod -n kube-system <apiserver-pod> -o yaml | grep -A30 enable-admission-plugins

kind/kubeadm 클러스터의 apiserver 정적 파드에서 확인.

# ValidatingAdmissionPolicy로 "limits 필수" 적용 후
kubectl apply -f require-limits-policy.yaml
kubectl run bad --image=nginx:1.27 --restart=Never    # limits 없음
# → 거부: "모든 컨테이너는 resources.limits가 필요합니다"

확인할 것: limits 없는 Pod 생성이 정책에 거부됨.

어드미션 체인의 정확한 순서 — Mutating이 먼저인 이유

인가(RBAC) 통과 후, 어드미션은 두 단계로 동작하며 순서가 보장된다:

flowchart LR
    REQ["요청 (인가 통과)"] --> M["1. Mutating Webhook (변형)<br/>객체를 고침"]
    M --> O["객체 직렬화 (고쳐진 최종 형태)"]
    O --> V["2. Validating Webhook / VAP (검증)<br/>고쳐진 객체를 검사"]
    V --> ETCD["3. etcd 저장"]

왜 Mutating이 먼저인가? 고쳐진 객체를 검증해야 하기 때문. Mutating이 기본 라벨을 추가하고, 그 다음 Validating이 "라벨이 있는가?"를 검증. 순서가 반대면 — Validating이 원본을 보고 통과시킨 뒤 Mutating이 바꿔서, 검증되지 않은 형태가 저장될 수 있다.

이 순서가 실무에서 중요한 순간: Mutating webhook이 image 태그를 digest로 바꾸고(공급망), Validating이 "신뢰된 레지스트리인가?"를 검증. Mutating→Validating 순서여야 바뀐 digest가 검증 대상이 된다.

내장 어드미션 컨트롤러도 이 순서를 따른다. ServiceAccount(Mutating)가 SA를 자동 마운트한 뒤, ValidatingAdmissionPolicy가 검증. 전체 체인의 순서가 정책의 정확성을 결정.

webhook의 failurePolicy — 정책 서버가 죽었을 때

동적 webhook(외부 서버)에 의존하면, 그 서버가 죽었을 때 어떻게 해야 하나가 관건. failurePolicy 설정:

  • Fail(기본): webhook 서버가 응답 안 하면 요청 거부. 보안 강하지만 — 서버 장애 시 클러스터 전체가 객체 생성 불가.
  • Ignore: 서버 응답 없으면 통과. 가용성 우선이지만 — 정책이 우회될 수 있음(보안 약화).
webhooks:
- failurePolicy: Fail     # 또는 Ignore

이 trade-off가 실무에서 날카롭다. 보안 정책 서버가 SPOF가 되면, Fail 모드에서 그 서버 장애 = 클러스터 마비. 그래서 webhook 서버의 고가용성(다중 replica)이 필수. ValidatingAdmissionPolicy(CEL, apiserver 내장)가 이 문제를 근본적으로 피하는 이유 — 외부 서버 의존이 없으니 failurePolicy 딜레마 자체가 안 생긴다.

CEL vs Rego(OPA) — 표현력과 단순성의 trade-off

ValidatingAdmissionPolicy의 CEL과 OPA Gatekeeper의 Rego는 다른 언어·다른 철학:

  CEL (VAP) Rego (OPA)
위치 apiserver 내장 외부 webhook 서버
언어 단순 표현식 (C-like) 논리 프로그래밍 (Datalog 계열)
학습 곡선 완만 가파름
표현력 제한적(복잡한 논리는 한계) 강력(데이터 쿼리, 복잡 규칙)
가용성 apiserver 일부(SPOF 없음) 별도 서버(SPOF/스케일 고려)
생태계 Kubernetes 내장 OPA 생태계(정책 공유/bundle)

선택: 단순 정책(필수 라벨, 자원 제한, 이미지 레지스트리)은 CEL로 충분 + 외부 의존 없음 이점. 복잡한 정책(조직 전체 정책, 데이터 기반 의사결정, 기존 OPA 투자)은 Rego. 실무에선 둘을 섞어 쓰기도 — 단순은 CEL, 복잡은 OPA.

실무 정책 패턴 — 현장에서 자주 쓰는 어드미션 규칙

어드미션 정책이 강제하는 흔한 규칙들을 구체적으로:

  1. 자원 제한 필수: limits 없는 Pod 거부. 리소스 부족/노드 과밀 방지(11장 QoS 연계).
  2. 이미지 레지스트리 화이트리스트: registry.company.com/*만 허용. 외부 이미지 차단(공급망).
  3. privileged Pod 금부: privileged: true 차단. 05장 Pod Security Admission과 겹치나, 정책으로 더 세밀하게.
  4. 필수 라벨: team/cost-center 라벨 없는 객체 거부. 비용 추적/소유권.
  5. namespace 할당량: 한 namespace의 Pod/Service 총량 제한. ResourceQuota와 보완.
  6. Ingress 호스트 충돌 방지: 같은 호스트를 두 Ingress가 주장하지 않게.

이런 정책들이 코드(YAML) 로 관리되면 — 감사 가능, 버전 관리, 자동 적용. "운영자가 수동으로 검사"에서 "정책이 자동 강제"로의 전환이 어드미션 컨트롤의 가치.

webhook timeoutSeconds — 정책 서버가 느릴 때

어드미션 webhook이 외부 서버에 HTTP 요청을 보내는데, 그 서버가 느리면 어떻게 되나. timeoutSeconds가 그 한계:

webhooks:
- timeoutSeconds: 3      # 기본 10초, 최대 30초

3초 안에 응답 없으면 → failurePolicy 적용(Fail이면 거부, Ignore면 통과). 이 timeout이 전체 클러스터 객체 생성 속도에 영향 — webhook이 30초 timeout이면, 객체 생성이 최대 30초 지연될 수 있다. 짧은 timeout + 빠른 webhook 서버가 사용자 경험에 중요.

dry-run — 어드미션 정책을 거칠지 미리 보기

kubectl apply --dry-run=server가 어드미션 체인을 거치되 etcd에 저장 안 함. "이 매니페스트가 정책을 통과할까?"를 실제로 검증 without side effect. 정책 도입 시 유용 — 새 정책이 기존 워크로드를 거부할지 dry-run으로 확인.

어드미션과 audit의 연결 — 거부를 추적

어드미션이 거부한 요청이 audit log에 기록된다. "왜 이 Pod가 안 만들어지나"를 audit에서 찾을 수 있음 — 어드미션 webhook/VAP의 거부 사유가 audit 이벤트에. 이것이 "정책이 거부했는데 이유를 모르겠다"를 audit log로 추적하는 경로.

흔히 묻는 것, 흔히 틀리는 것

오해 정정
"인가(RBAC) 통과하면 끝이다" 어드미션 컨트롤이 한 번 더 (형태 검사)
"Validating은 객체를 고친다" 검사만. 고치는 건 Mutating
"어드미션 정책은 항상 webhook 서버가 필요하다" ValidatingAdmissionPolicy(1.30 GA)는 CEL로 apiserver 내장
"Mutating과 Validating 순서는 무관하다" Mutating 먼저 → 그 후 Validating
"CEL과 OPA는 같다" 다른 언어/생태계. CEL은 가벼움, OPA는 표현력/생태계
"어드미션이 거부하면 403이다" 보통 403 아닌 별도 거부 응답. 디버깅 시 이벤트 확인

요약 — 이 글의 결론

  • 어드미션 컨트롤이 인가(RBAC) etcd 저장 에 형태를 검문 — "권한은 있지만 이 모양은 안 된다"를 강제.
  • Mutating(변형) → Validating(검증) 순서. Mutating이 고치고 Validating이 검사.
  • 내장(apiserver 컴파일) + 동적(webhook). 동적 webhook(OPA Gatekeeper/Kyverno)이 확장성.
  • ValidatingAdmissionPolicy(CEL, 1.30 GA)가 webhook 없이 정책을 apiserver 내장 평가. 별도 서버/지연 부담 없음.
  • MutatingAdmissionPolicy(1.36)가 변형도 CEL 정책으로 확장.
  • CEL vs OPA: CEL은 가벼움/apiserver 내장, OPA는 표현력/생태계. 팀 요구로 선택.

생각해 볼 문제

  1. RBAC은 "누가"만 다룬다. "만들어지는 객체가 어떤 모양이어야"를 강제하려면 어느 메커니즘인가?
  2. Mutating webhook이 객체를 고쳤다. 그 후 Validating이 거부했다. 결과는? (롤백)
  3. OPA Gatekeeper(webhook)를 쓰다 ValidatingAdmissionPolicy로 옮기면 얻는 것/잃는 것?
  4. "신뢰된 레지스트리 이미지만" 정책을 ValidatingAdmissionPolicy CEL로 짜 보라(의사코드).
  5. 어드미션 webhook 서버가 죽었다. 클러스터는 어떻게 되나? (failurePolicy)
  6. Mutating webhook이 라벨을 자동 추가했다. 사용자가 모르는 변형의 위험은?

참고