쿠버네티스 장애·보안 사고의 뿌리가 애플리케이션 코드가 아니라 클러스터 설정(매니페스트) 실수인 경우를 자주 봤습니다. 이 글에서는 정책 위반이 왜 ‘너무 늦게’ 발견되는지와, 보안/플랫폼 팀 관점에서 CI(Shift-left)와 Admission(최종 강제)을 어떻게 배분하는 게 현실적인지 정리합니다.
쿠버네티스 정책 위반이 “배포 이후”에 발견되는 이유
쿠버네티스에서 문제가 되는 지점은 코드보다 운영 설정인 경우가 많습니다.
- 리소스 제한 누락(
requests/limits) → 노이즈 네이버(Neighbor) 문제, OOMKilled, 노드 압박 securityContext부재(예:runAsNonRoot,readOnlyRootFilesystem) → 권한 상승/침해 범위 확대- RBAC 과다 권한 → 계정 탈취 시 피해 폭발
- 네임스페이스/네트워크 정책 미적용 → 의도치 않은 통신 경로 생성
그런데 정책이 늦게 터지는 구조적 이유가 있습니다.
1) “정책”이 코드 리뷰 경로 밖에 있는 경우가 많습니다
대부분의 조직에서 앱 코드는 PR로 검증하지만, 매니페스트/Helm values는 상대적으로 느슨하게 넘어가거나(“어차피 운영에서 잡겠지”), 리뷰어도 정책 세부를 다 알기 어렵습니다.
2) 클러스터에서만 보이는 정보가 있어 “배포 전 재현”이 어렵습니다
예를 들어 RBAC, PodSecurity, 네임스페이스 기본 설정, 기존 리소스와의 충돌 같은 것들은 로컬에서 kubectl apply만으로는 완전히 검증하기 어렵습니다. 그러다 보니 정책 검증을 클러스터 Admission에만 의존하게 됩니다.
3) Admission은 강력하지만, 개발자에게는 “마지막 순간의 벽”이 됩니다
Admission(Webhook/Gatekeeper/Kyverno 등)에서 차단되면 확실히 안전하지만, 개발자 입장에서는 배포 직전에 막혀서 원인 파악·수정·재시도 비용이 커집니다. 결국 플랫폼/보안팀에도 “긴급 예외 요청”이 몰리며 운영부채가 쌓입니다.
해법의 핵심: “사후 차단”만 하지 말고, 정책을 코드로 앞당기기(Shift-left)
현실적인 해법은 한 줄로 요약하면 이렇습니다.
- PR/CI 단계에서 정책을 ‘빠르게 피드백’(테스트/린트/드라이런)하고
- **Admission 단계에서 ‘최종 강제’**하여 우회 배포를 막습니다.
즉, 정책을 “한 곳에서만” 집행하지 말고, **개발 단계(빠른 피드백)**와 **배포 단계(강제 차단)**에 역할을 나눕니다.
어디에 무엇을 두나: CI(Shift-left) vs Admission(최종 강제) 현실적 배분안
보안/플랫폼 팀이 정책을 설계할 때 가장 중요한 건 “모든 걸 다 막자”가 아니라 마찰을 최소화하면서 안전을 최대화하는 배치입니다. 저는 다음처럼 나누는 접근이 현실적이라고 봅니다.
1) CI/PR에서 먼저 잡아야 하는 것(빠른 피드백에 최적)
개발자가 수정하기 쉬운 정적 규칙은 CI에서 먼저 잡는 게 효율적입니다.
- 리소스
requests/limits필수 runAsNonRoot,readOnlyRootFilesystem등 기본 보안 컨텍스트latest태그 금지, 허용 레지스트리 제한(조직 정책)- 금지된 capability, privileged 컨테이너 금지
- 기본 라벨/어노테이션 규칙(추적성/소유자/비용태깅)
CI에서 중요한 건 “완벽한 차단”이 아니라 개발자가 PR 단계에서 바로 수정 가능한 형태의 피드백입니다.
예시: 서버 사이드 드라이런으로 “실제 클러스터 기준” 검증하기
로컬 스키마 검증만으로 부족할 때가 많아서, 저는 가능하면 CI에서 아래를 섞는 편입니다.
# 실제 API 서버 기준으로 검증(리소스 스키마/Admission 일부 반영)
kubectl apply --dry-run=server -f k8s/manifest.yaml
- 장점: “내 컴퓨터에선 되는데요?”를 줄입니다.
- 주의: 클러스터 접근 권한/네트워크가 필요하고, 환경별 차이를 관리해야 합니다.
2) Admission에서 반드시 강제해야 하는 것(우회 불가 ‘최후의 문’)
반대로, 조직 리스크를 직접 키우는 정책은 Admission에서 강제해야 합니다. CI만으로는 우회가 가능하기 때문입니다(핫픽스, 수동 적용, 다른 파이프라인 등).
- privileged/hostPath/hostNetwork 같은 고위험 워크로드 금지
- 특정 네임스페이스 보호(예:
kube-system류) - RBAC 과권한 생성 차단(특정 verbs/resources 조합)
- 승인된 레지스트리/서명된 이미지 강제(공급망 보안)
- 멀티테넌시 경계(네임스페이스 격리, 네트워크 정책 필수 등)
Admission의 목표는 “친절한 피드백”이 아니라 절대 배포되면 안 되는 것의 차단입니다.
예시: Kyverno로 “requests/limits 필수” 정책 강제(간단 예시)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-requests-limits
spec:
validationFailureAction: Enforce
rules:
- name: check-resources
match:
resources:
kinds:
- Pod
validate:
message: "모든 컨테이너에 resources.requests/limits를 지정해야 합니다."
pattern:
spec:
containers:
- resources:
requests:
memory: "?*"
cpu: "?*"
limits:
memory: "?*"
cpu: "?*"
Enforce는 “배포 차단” 모드입니다.- 같은 규칙이라도 CI에서는 “경고/리포트”로 먼저 시작하고, 성숙해지면 Enforce로 올리는 전략이 좋습니다.
운영팀 관점 인사이트: “정책의 배치”는 기술 문제가 아니라 제품 설계 문제입니다
정책을 앞당기려다 실패하는 패턴도 자주 봅니다.
- 초기부터 너무 많은 Enforce → 개발자 경험 악화, 예외 요청 폭증, 정책 무력화
- CI에서만 검사 → 긴급 수동 배포/다른 파이프라인으로 우회 가능, 결국 사고는 남
- 정책 메시지가 불친절 → “뭘 어떻게 고치라는 건지” 몰라서 지원 티켓만 증가
그래서 저는 정책을 “제품”처럼 다뤄야 한다고 생각합니다.
- **규칙별 소유자(Owner)**와 목적(보안/신뢰성/비용)을 명확히 합니다.
- 경고(Audit) → 차단(Enforce) 로 점진적으로 올립니다.
- 예외는 “슬랙 DM”이 아니라 만료일이 있는 승인 워크플로로 관리합니다.
- 무엇보다 정책 실패 메시지는 “왜”보다 **“어떻게 고치는지”**가 먼저 나와야 합니다(예: 수정 예시, 링크, 템플릿).
마무리
쿠버네티스 정책 위반이 늦게 발견되는 건 개인의 실수라기보다 정책이 개발 흐름 안으로 들어오지 못한 구조 때문입니다. PR/CI에서 빠른 피드백으로 마찰을 줄이고, Admission에서 최종 강제로 우회를 막는 이중 구조로 가져가면, 보안과 개발 속도를 동시에 챙길 수 있습니다.
