Skip to content

Split View: Kubernetes RBAC 심층 가이드: Role·ClusterRole·OPA Gatekeeper로 구현하는 최소 권한 접근제어

✨ Learn with Quiz
|

Kubernetes RBAC 심층 가이드: Role·ClusterRole·OPA Gatekeeper로 구현하는 최소 권한 접근제어

Kubernetes RBAC OPA Gatekeeper

들어가며

Kubernetes 클러스터를 프로덕션에서 운영하면서 가장 먼저 체계를 잡아야 하는 영역이 접근제어(Access Control) 이다. 클러스터 관리자, 개발자, CI/CD 파이프라인, 모니터링 에이전트 등 다양한 주체가 API Server에 요청을 보내며, 이 요청들을 누가(Subject) 어떤 리소스(Resource)에 어떤 동작(Verb)을 수행할 수 있는가로 세밀하게 제어해야 한다. Kubernetes가 제공하는 기본 인가 메커니즘인 RBAC(Role-Based Access Control)만으로도 상당한 수준의 접근제어가 가능하지만, "특정 이미지 레지스트리만 허용", "모든 Pod에 리소스 제한 필수 설정", "특정 라벨이 없는 네임스페이스 생성 금지"와 같은 정책(Policy) 기반 제어는 RBAC의 영역을 벗어난다.

이 간극을 채우는 것이 바로 OPA Gatekeeper이다. Open Policy Agent(OPA) 기반의 Kubernetes 어드미션 컨트롤러로, Rego 언어로 작성한 정책을 ConstraintTemplate과 Constraint CRD를 통해 클러스터에 적용한다. RBAC이 "누구에게 어떤 권한을 부여할 것인가"를 다룬다면, OPA Gatekeeper는 "허용된 요청이라도 정책을 준수하는가"를 검증하는 보완 계층이다.

이 글에서는 RBAC의 핵심 구성 요소부터 ServiceAccount 토큰 관리, 네임스페이스별 권한 격리 설계 패턴, OPA Gatekeeper 아키텍처와 Rego 정책 작성, 실전 정책 사례, 감사(Audit) 전략, 주요 정책 엔진 비교, 그리고 장애 사례와 복구 절차까지 종합적으로 다룬다.

RBAC 핵심 개념

Kubernetes RBAC은 네 가지 API 오브젝트로 구성된다. 이 네 가지의 관계를 정확히 이해하는 것이 접근제어 설계의 출발점이다.

Role과 ClusterRole

Role은 특정 네임스페이스 안에서 리소스에 대한 권한 규칙(rules)을 정의한다. ClusterRole은 네임스페이스에 국한되지 않으며 클러스터 전역 리소스(nodes, persistentvolumes 등)에 대한 권한이나 여러 네임스페이스에 걸쳐 재사용할 권한 세트를 정의할 때 사용한다.

# 네임스페이스 범위 Role: dev 네임스페이스에서 Pod 읽기 권한
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: dev
  name: pod-reader
rules:
  - apiGroups: ['']
    resources: ['pods']
    verbs: ['get', 'watch', 'list']
  - apiGroups: ['']
    resources: ['pods/log']
    verbs: ['get']
# 클러스터 범위 ClusterRole: 모든 네임스페이스에서 Deployment 관리
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: deployment-manager
rules:
  - apiGroups: ['apps']
    resources: ['deployments']
    verbs: ['get', 'list', 'watch', 'create', 'update', 'patch']
  - apiGroups: ['apps']
    resources: ['deployments/scale']
    verbs: ['update', 'patch']

핵심 원칙은 와일드카드(*) 사용을 최대한 회피하는 것이다. resources: ["*"]verbs: ["*"]를 사용하면 현재는 물론 미래에 추가될 리소스에 대해서도 무제한 접근을 허용하게 되어 보안 위험이 급격히 증가한다.

RoleBinding과 ClusterRoleBinding

정의된 Role/ClusterRole을 실제 주체(Subject)에 연결하는 것이 RoleBinding과 ClusterRoleBinding이다.

# RoleBinding: dev 네임스페이스에서 frontend-team 그룹에 pod-reader Role 부여
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: frontend-pod-reader
  namespace: dev
subjects:
  - kind: Group
    name: frontend-team
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

주의사항: system:masters 그룹에 사용자를 추가하는 것은 절대 피해야 한다. 이 그룹의 멤버는 모든 RBAC 검사를 우회하며, RoleBinding이나 ClusterRoleBinding을 삭제해도 권한을 회수할 수 없다.

Subject 유형

RBAC에서 권한을 부여받는 주체(Subject)는 세 가지 유형이 있다.

  • User: 외부 인증 시스템(OIDC, 인증서 등)이 제공하는 사용자 아이덴티티
  • Group: 사용자들의 논리적 그룹. 인증 시스템에서 그룹 정보를 전달
  • ServiceAccount: Kubernetes가 직접 관리하는 Pod 내부 워크로드용 계정

ServiceAccount와 토큰 관리

Kubernetes 1.24부터 ServiceAccount에 자동으로 영구 토큰이 생성되지 않는다. TokenRequest API를 통해 시간 제한이 있는 토큰을 발급받는 것이 기본 동작이며, 이는 보안 측면에서 중요한 개선이다.

# ServiceAccount 생성
kubectl create serviceaccount ci-deployer -n staging

# 시간 제한 토큰 발급 (1시간 유효)
kubectl create token ci-deployer -n staging --duration=3600s

# ServiceAccount 권한 확인
kubectl auth can-i create deployments --as=system:serviceaccount:staging:ci-deployer -n staging

# 특정 ServiceAccount에 바인딩된 Role 확인
kubectl get rolebindings -n staging -o json | \
  jq '.items[] | select(.subjects[]? | .name=="ci-deployer" and .kind=="ServiceAccount")'

CI/CD 파이프라인용 ServiceAccount를 설계할 때는 다음 원칙을 따른다.

  1. 파이프라인별 전용 ServiceAccount 생성: 하나의 ServiceAccount를 여러 파이프라인에서 공유하지 않는다
  2. 필요한 네임스페이스에만 RoleBinding 생성: ClusterRoleBinding 대신 네임스페이스별 RoleBinding을 사용한다
  3. 토큰 유효 기간 최소화: 파이프라인 실행 시간에 맞춰 토큰 유효 기간을 설정한다
  4. automountServiceAccountToken 비활성화: Pod에서 불필요하게 ServiceAccount 토큰이 마운트되지 않도록 설정한다
# automountServiceAccountToken 비활성화 예시
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-service
  namespace: production
automountServiceAccountToken: false

RBAC 설계 패턴

네임스페이스별 격리

멀티 테넌트 환경에서는 네임스페이스를 팀 또는 환경 단위로 분리하고, 각 네임스페이스에 독립적인 RBAC 정책을 적용한다.

# 팀별 네임스페이스 + RBAC 일괄 구성 예시
---
apiVersion: v1
kind: Namespace
metadata:
  name: team-alpha
  labels:
    team: alpha
    environment: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: team-alpha
  name: team-alpha-developer
rules:
  - apiGroups: ['', 'apps', 'batch']
    resources: ['pods', 'deployments', 'services', 'configmaps', 'jobs']
    verbs: ['get', 'list', 'watch', 'create', 'update', 'patch', 'delete']
  - apiGroups: ['']
    resources: ['secrets']
    verbs: ['get', 'list'] # Secret 쓰기 권한은 제한
  - apiGroups: ['']
    resources: ['pods/exec']
    verbs: ['create'] # 디버깅을 위한 exec 허용
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: team-alpha-developer-binding
  namespace: team-alpha
subjects:
  - kind: Group
    name: team-alpha-devs
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: team-alpha-developer
  apiGroup: rbac.authorization.k8s.io

권한 에스컬레이션 방지

RBAC 설계에서 가장 주의해야 할 것은 권한 에스컬레이션(Privilege Escalation) 경로를 차단하는 것이다. 다음 권한들은 특별히 주의가 필요하다.

  • pods/exec: Pod 내부에서 임의 명령을 실행할 수 있어, Pod의 ServiceAccount 토큰 탈취 가능
  • secrets 읽기: 다른 ServiceAccount의 토큰이나 데이터베이스 자격 증명 노출 위험
  • create on rolebindings/clusterrolebindings: 스스로에게 상위 권한 부여 가능
  • escalate/bind verb: Role/ClusterRole을 수정하거나 바인딩할 수 있는 메타 권한

OPA Gatekeeper 아키텍처

OPA Gatekeeper는 Kubernetes 어드미션 웹훅(Admission Webhook)으로 동작하며, API Server가 리소스를 생성/수정/삭제하기 전에 정책 검증을 수행한다.

구성 요소

  1. Gatekeeper Controller Manager: 어드미션 웹훅 요청을 처리하고 Rego 정책을 평가하는 핵심 컴포넌트
  2. Audit Controller: 기존에 배포된 리소스가 정책을 준수하는지 주기적으로 검사
  3. ConstraintTemplate CRD: Rego 정책 로직과 파라미터 스키마를 정의하는 템플릿
  4. Constraint CRD: ConstraintTemplate을 인스턴스화하여 구체적인 정책 적용 대상과 파라미터를 지정

설치

# Gatekeeper 설치 (Helm)
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update

helm install gatekeeper gatekeeper/gatekeeper \
  --namespace gatekeeper-system \
  --create-namespace \
  --set audit.interval=60 \
  --set constraintViolationsLimit=50 \
  --set audit.fromCache=true

# 설치 확인
kubectl get pods -n gatekeeper-system
kubectl get crd | grep gatekeeper

동작 흐름

Gatekeeper의 정책 평가 흐름은 다음과 같다.

  1. 사용자 또는 컨트롤러가 API Server에 리소스 생성/수정 요청을 전송
  2. API Server가 인증(AuthN)과 인가(AuthZ, RBAC)를 수행
  3. 어드미션 단계에서 Gatekeeper 웹훅으로 요청 전달
  4. Gatekeeper가 해당 리소스에 매칭되는 Constraint를 찾아 Rego 정책 평가
  5. 위반 사항이 있으면 요청 거부(Deny)와 함께 위반 메시지 반환
  6. 위반 없으면 요청 승인(Allow)하여 etcd에 저장

ConstraintTemplate과 Rego 정책 작성

기본 구조

ConstraintTemplate은 두 부분으로 구성된다. CRD 스펙(파라미터 스키마)과 Rego 코드(정책 로직)이다.

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels:
              type: array
              items:
                type: string
              description: '필수 라벨 목록'
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels

        violation[{"msg": msg, "details": {"missing_labels": missing}}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("리소스에 필수 라벨이 누락되었습니다: %v", [missing])
        }

Constraint 적용

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-team-label
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: ['']
        kinds: ['Namespace']
    excludedNamespaces:
      - kube-system
      - gatekeeper-system
  parameters:
    labels:
      - 'team'
      - 'cost-center'

실전 정책 사례

사례 1: 이미지 레지스트리 제한

프로덕션 클러스터에서 허용된 컨테이너 레지스트리에서만 이미지를 가져오도록 강제한다.

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedrepos
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRepos
      validation:
        openAPIV3Schema:
          type: object
          properties:
            repos:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedrepos

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not image_from_allowed(container.image)
          msg := sprintf("컨테이너 '%v'의 이미지 '%v'는 허용되지 않은 레지스트리입니다. 허용 레지스트리: %v",
            [container.name, container.image, input.parameters.repos])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.initContainers[_]
          not image_from_allowed(container.image)
          msg := sprintf("initContainer '%v'의 이미지 '%v'는 허용되지 않은 레지스트리입니다.",
            [container.name, container.image])
        }

        image_from_allowed(image) {
          repo := input.parameters.repos[_]
          startswith(image, repo)
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: prod-allowed-repos
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: ['']
        kinds: ['Pod']
    namespaces:
      - production
      - staging
  parameters:
    repos:
      - 'gcr.io/my-company/'
      - 'us-docker.pkg.dev/my-company/'
      - 'registry.internal.company.com/'

사례 2: 리소스 요청/제한 필수 설정

모든 컨테이너에 CPU와 메모리의 requests/limits 설정을 강제한다.

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequireresourcelimits
spec:
  crd:
    spec:
      names:
        kind: K8sRequireResourceLimits
      validation:
        openAPIV3Schema:
          type: object
          properties:
            requiredResources:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequireresourcelimits

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          resource := input.parameters.requiredResources[_]
          not container.resources.limits[resource]
          msg := sprintf("컨테이너 '%v'에 resources.limits.%v가 설정되지 않았습니다.", [container.name, resource])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          resource := input.parameters.requiredResources[_]
          not container.resources.requests[resource]
          msg := sprintf("컨테이너 '%v'에 resources.requests.%v가 설정되지 않았습니다.", [container.name, resource])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireResourceLimits
metadata:
  name: require-cpu-memory-limits
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: ['']
        kinds: ['Pod']
    excludedNamespaces:
      - kube-system
      - gatekeeper-system
  parameters:
    requiredResources:
      - 'cpu'
      - 'memory'

사례 3: 특권 컨테이너 차단

특권 모드(privileged)로 실행되는 컨테이너를 차단한다.

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sdisallowprivileged
spec:
  crd:
    spec:
      names:
        kind: K8sDisallowPrivileged
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sdisallowprivileged

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          container.securityContext.privileged == true
          msg := sprintf("특권 컨테이너는 허용되지 않습니다: '%v'", [container.name])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.initContainers[_]
          container.securityContext.privileged == true
          msg := sprintf("특권 initContainer는 허용되지 않습니다: '%v'", [container.name])
        }

RBAC 감사(Audit) 및 모니터링

kubectl 기반 감사

# 현재 사용자의 권한 확인
kubectl auth can-i --list

# 특정 ServiceAccount의 네임스페이스 권한 확인
kubectl auth can-i --list --as=system:serviceaccount:production:app-deployer -n production

# 특정 동작 가능 여부 확인
kubectl auth can-i delete pods --as=system:serviceaccount:staging:ci-runner -n staging

# 클러스터 전체에서 secrets 읽기 가능한 주체 탐색 (kubectl-who-can 플러그인)
kubectl who-can get secrets --all-namespaces

# 과도한 권한을 가진 ClusterRoleBinding 탐색
kubectl get clusterrolebindings -o json | \
  jq '.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name, .subjects'

Gatekeeper 감사(Audit) 활용

Gatekeeper의 Audit 기능은 기존에 배포된 리소스에 대해서도 정책 위반 여부를 검사한다. enforcementAction: dryrun으로 설정하면 차단하지 않고 위반 사항만 기록할 수 있다.

# 정책 위반 현황 조회
kubectl get k8sallowedrepos prod-allowed-repos -o yaml | \
  grep -A 100 "status:" | head -50

# 전체 Constraint 위반 요약
kubectl get constraints -o json | \
  jq '.items[] | {name: .metadata.name, kind: .kind, violations: (.status.totalViolations // 0)}'

모니터링 통합

Gatekeeper는 Prometheus 메트릭을 기본으로 노출한다.

  • gatekeeper_violations: 현재 감사에서 발견된 위반 수
  • gatekeeper_request_duration_seconds: 어드미션 요청 처리 시간
  • gatekeeper_request_count: 총 어드미션 요청 수 (허용/거부별)
  • gatekeeper_constraint_template_status: ConstraintTemplate 상태

비교표: RBAC vs OPA Gatekeeper vs PSA vs Kyverno

Kubernetes 접근제어 및 정책 엔진을 종합적으로 비교한다.

항목RBACOPA GatekeeperPod Security Admission (PSA)Kyverno
동작 계층인가(Authorization)어드미션(Admission)어드미션(Admission)어드미션(Admission)
정책 언어YAML 선언형Rego내장 프로필 (Privileged/Baseline/Restricted)YAML 선언형
학습 곡선낮음높음 (Rego 학습 필요)매우 낮음낮음
유연성낮음 (권한 부여/거부만)매우 높음낮음 (3개 프로필만)높음
Mutation 지원해당 없음지원 (Assign/Modify)미지원지원 (mutate)
리소스 생성해당 없음미지원미지원지원 (generate)
감사(Audit)audit log 분석 필요내장 Audit 기능audit/warn 모드PolicyReport CRD
CNCF 단계Kubernetes 내장GraduatedKubernetes 내장Incubating
리소스 소비없음 (API Server 내장)높음 (다수 Pod)매우 낮음중간
추천 용도기본 접근제어복잡한 정책 로직Pod 보안 기준 강제Kubernetes 네이티브 정책

권장 조합: RBAC(기본 인가) + PSA(Pod 보안 기준) + Gatekeeper 또는 Kyverno(커스텀 정책)를 함께 사용하는 것이 프로덕션 환경의 모범 사례이다. 단순한 정책에는 Kyverno, 복잡한 교차 리소스 검증에는 OPA Gatekeeper가 적합하다.

장애 사례 및 복구 절차

사례 1: Gatekeeper 웹훅 장애로 전체 배포 차단

증상: 모든 Pod 생성이 거부되며, 에러 메시지에 webhook "validation.gatekeeper.sh" denied the request 또는 연결 타임아웃이 표시된다.

원인: Gatekeeper Controller Pod가 비정상 종료되었거나, 리소스 부족으로 응답하지 못하는 상태이다.

복구 절차:

# 1. Gatekeeper Pod 상태 확인
kubectl get pods -n gatekeeper-system

# 2. 긴급 상황: 웹훅 일시 비활성화 (failurePolicy를 Ignore로 변경)
kubectl get validatingwebhookconfigurations gatekeeper-validating-webhook-configuration -o yaml > webhook-backup.yaml
kubectl patch validatingwebhookconfigurations gatekeeper-validating-webhook-configuration \
  --type='json' -p='[{"op": "replace", "path": "/webhooks/0/failurePolicy", "value": "Ignore"}]'

# 3. Gatekeeper Pod 재시작
kubectl rollout restart deployment gatekeeper-controller-manager -n gatekeeper-system

# 4. 정상화 확인 후 failurePolicy 복원
kubectl apply -f webhook-backup.yaml

예방 조치: Gatekeeper의 failurePolicyFail에서 Ignore로 변경하면 가용성은 높아지지만 정책 우회가 가능해지므로, 프로덕션에서는 Fail을 유지하되 Gatekeeper Pod의 리소스와 replicas를 충분히 확보한다.

사례 2: 과도한 ClusterRoleBinding으로 권한 누수

증상: 모든 ServiceAccount가 클러스터 리소스를 읽을 수 있는 비정상적인 상태가 발견된다.

진단 및 복구:

# cluster-admin 바인딩된 주체 탐색
kubectl get clusterrolebindings -o json | \
  jq '.items[] | select(.roleRef.name=="cluster-admin") | {name: .metadata.name, subjects: .subjects}'

# 불필요한 ClusterRoleBinding 제거
kubectl delete clusterrolebinding suspicious-admin-binding

# 전체 ClusterRoleBinding 감사
kubectl get clusterrolebindings -o json | \
  jq '.items[] | {name: .metadata.name, role: .roleRef.name, subjects: [.subjects[]? | .kind + ":" + .name]}'

사례 3: ConstraintTemplate 구문 오류로 정책 미작동

증상: Constraint를 생성했지만 정책이 적용되지 않는다. kubectl get constrainttemplates 에서 STATUS가 비정상이다.

진단:

# ConstraintTemplate 상태 확인
kubectl get constrainttemplate k8srequiredlabels -o json | jq '.status'

# Rego 구문 오류 확인
kubectl describe constrainttemplate k8srequiredlabels | grep -A 10 "Status:"

# Gatekeeper 로그에서 오류 확인
kubectl logs -n gatekeeper-system -l control-plane=controller-manager --tail=100

운영 체크리스트

프로덕션 Kubernetes 클러스터의 접근제어를 점검하기 위한 체크리스트이다.

RBAC 체크리스트

  • system:masters 그룹에 불필요한 사용자가 등록되어 있지 않은가
  • cluster-admin ClusterRole이 바인딩된 주체를 모두 파악하고 있는가
  • 와일드카드(*) 권한을 사용하는 Role/ClusterRole이 존재하는가
  • ServiceAccount에 automountServiceAccountToken: false가 기본 설정되어 있는가
  • 네임스페이스별로 적절한 Role/RoleBinding이 구성되어 있는가
  • pods/exec, secrets, rolebindings 등 민감 권한이 최소한으로 부여되어 있는가
  • 사용하지 않는 ServiceAccount와 RoleBinding이 정리되어 있는가
  • RBAC 설정이 Git에 관리되고 있는가 (GitOps)

OPA Gatekeeper 체크리스트

  • Gatekeeper Controller가 고가용성(replicas 2 이상)으로 배포되어 있는가
  • failurePolicy가 프로덕션 요구사항에 맞게 설정되어 있는가
  • kube-system, gatekeeper-system 등 시스템 네임스페이스가 적절히 제외되어 있는가
  • 새 정책을 dryrun 모드로 먼저 테스트하는 프로세스가 있는가
  • Audit 결과를 정기적으로 검토하고 있는가
  • ConstraintTemplate의 Rego 코드가 단위 테스트를 통과하는가
  • Gatekeeper 메트릭이 Prometheus/Grafana에 통합되어 있는가
  • 웹훅 장애 시 긴급 복구 절차(Runbook)가 문서화되어 있는가

정기 감사 항목

  • 분기별 RBAC 권한 리뷰: 과도한 권한 보유 주체 식별
  • 월별 Gatekeeper Audit 리포트: 정책 위반 리소스 현황
  • ServiceAccount 토큰 사용 패턴 분석: 미사용 토큰 정리
  • 신규 CRD/API 도입 시 RBAC 및 Gatekeeper 정책 업데이트

마치며

Kubernetes 접근제어는 RBAC만으로 완성되지 않는다. RBAC은 "누구에게 어떤 권한을 부여할 것인가"에 대한 답을 제공하지만, "허용된 작업이 정책을 준수하는가"를 검증하려면 OPA Gatekeeper와 같은 어드미션 컨트롤러가 필수적이다. 두 메커니즘을 함께 사용하고, Pod Security Admission으로 기본 보안 기준을 강제하며, 정기적인 감사를 수행하는 것이 프로덕션 환경의 최소 권한 원칙을 실현하는 방법이다.

특히 정책을 코드로 관리(Policy as Code)하고, 변경 전 반드시 dryrun 모드로 영향을 평가하며, 장애 시 긴급 복구 절차를 미리 준비하는 운영 습관이 보안 사고를 예방하는 핵심이다. RBAC과 OPA Gatekeeper는 기술적 도구이지만, 이를 효과적으로 운영하려면 조직의 접근제어 거버넌스와 연계해야 한다는 점을 잊지 말아야 한다.

Kubernetes RBAC Deep Dive: Implementing Least-Privilege Access Control with Role, ClusterRole, and OPA Gatekeeper

Kubernetes RBAC OPA Gatekeeper

Introduction

When operating Kubernetes clusters in production, access control is the first area that demands a well-structured framework. Cluster administrators, developers, CI/CD pipelines, monitoring agents, and many other entities send requests to the API Server, and these requests must be finely controlled to determine who (Subject) can perform which actions (Verb) on which resources (Resource). While Kubernetes' built-in authorization mechanism, RBAC (Role-Based Access Control), provides a substantial level of access control, policy-based controls such as "allow only specific image registries," "require resource limits on all Pods," or "prohibit namespace creation without specific labels" fall outside the scope of RBAC.

OPA Gatekeeper fills this gap. As a Kubernetes admission controller built on Open Policy Agent (OPA), it enforces policies written in the Rego language through ConstraintTemplate and Constraint CRDs applied to the cluster. While RBAC addresses "who should be granted which permissions," OPA Gatekeeper validates "whether an authorized request complies with policies" -- a complementary security layer.

This guide covers RBAC core components, ServiceAccount token management, namespace-level permission isolation design patterns, OPA Gatekeeper architecture and Rego policy authoring, real-world policy use cases, audit strategies, policy engine comparison, and failure case studies with recovery procedures.

RBAC Core Concepts

Kubernetes RBAC consists of four API objects. Understanding the relationships among these four is the starting point for access control design.

Role and ClusterRole

A Role defines permission rules for resources within a specific namespace. A ClusterRole is not scoped to any namespace and is used to define permissions for cluster-wide resources (nodes, persistentvolumes, etc.) or to create reusable permission sets across multiple namespaces.

# Namespace-scoped Role: Pod read access in the dev namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: dev
  name: pod-reader
rules:
  - apiGroups: ['']
    resources: ['pods']
    verbs: ['get', 'watch', 'list']
  - apiGroups: ['']
    resources: ['pods/log']
    verbs: ['get']
# Cluster-scoped ClusterRole: Deployment management across all namespaces
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: deployment-manager
rules:
  - apiGroups: ['apps']
    resources: ['deployments']
    verbs: ['get', 'list', 'watch', 'create', 'update', 'patch']
  - apiGroups: ['apps']
    resources: ['deployments/scale']
    verbs: ['update', 'patch']

The core principle is to avoid wildcard (*) usage as much as possible. Using resources: ["*"] or verbs: ["*"] grants unlimited access not only to current resources but also to any future resources that may be added, dramatically increasing security risk.

RoleBinding and ClusterRoleBinding

RoleBinding and ClusterRoleBinding connect defined Roles/ClusterRoles to actual Subjects.

# RoleBinding: Grant pod-reader Role to frontend-team group in dev namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: frontend-pod-reader
  namespace: dev
subjects:
  - kind: Group
    name: frontend-team
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

Critical warning: Never add users to the system:masters group. Members of this group bypass all RBAC checks, and their permissions cannot be revoked by removing RoleBindings or ClusterRoleBindings.

Subject Types

There are three types of Subjects that can receive permissions in RBAC:

  • User: A user identity provided by an external authentication system (OIDC, certificates, etc.)
  • Group: A logical grouping of users. Group information is conveyed by the authentication system
  • ServiceAccount: An in-cluster account managed directly by Kubernetes, used for workloads running inside Pods

ServiceAccount and Token Management

Since Kubernetes 1.24, ServiceAccounts no longer automatically generate permanent tokens. The TokenRequest API issues time-limited tokens by default, which is a significant security improvement.

# Create a ServiceAccount
kubectl create serviceaccount ci-deployer -n staging

# Issue a time-limited token (valid for 1 hour)
kubectl create token ci-deployer -n staging --duration=3600s

# Check ServiceAccount permissions
kubectl auth can-i create deployments --as=system:serviceaccount:staging:ci-deployer -n staging

# Find Roles bound to a specific ServiceAccount
kubectl get rolebindings -n staging -o json | \
  jq '.items[] | select(.subjects[]? | .name=="ci-deployer" and .kind=="ServiceAccount")'

When designing ServiceAccounts for CI/CD pipelines, follow these principles:

  1. Create dedicated ServiceAccounts per pipeline: Never share a single ServiceAccount across multiple pipelines
  2. Create RoleBindings only in required namespaces: Use namespace-scoped RoleBindings instead of ClusterRoleBindings
  3. Minimize token validity period: Set token expiration to match the pipeline execution duration
  4. Disable automountServiceAccountToken: Prevent unnecessary token mounting in Pods
# Disabling automountServiceAccountToken
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-service
  namespace: production
automountServiceAccountToken: false

RBAC Design Patterns

Namespace-Level Isolation

In multi-tenant environments, namespaces are separated by team or environment, with independent RBAC policies applied to each namespace.

# Team namespace + RBAC batch configuration example
---
apiVersion: v1
kind: Namespace
metadata:
  name: team-alpha
  labels:
    team: alpha
    environment: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: team-alpha
  name: team-alpha-developer
rules:
  - apiGroups: ['', 'apps', 'batch']
    resources: ['pods', 'deployments', 'services', 'configmaps', 'jobs']
    verbs: ['get', 'list', 'watch', 'create', 'update', 'patch', 'delete']
  - apiGroups: ['']
    resources: ['secrets']
    verbs: ['get', 'list'] # Restrict Secret write permissions
  - apiGroups: ['']
    resources: ['pods/exec']
    verbs: ['create'] # Allow exec for debugging
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: team-alpha-developer-binding
  namespace: team-alpha
subjects:
  - kind: Group
    name: team-alpha-devs
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: team-alpha-developer
  apiGroup: rbac.authorization.k8s.io

Preventing Privilege Escalation

The most critical aspect of RBAC design is blocking privilege escalation paths. The following permissions require special attention:

  • pods/exec: Allows arbitrary command execution inside Pods, enabling ServiceAccount token theft
  • secrets read access: Risk of exposing other ServiceAccount tokens or database credentials
  • create on rolebindings/clusterrolebindings: Allows self-granting elevated permissions
  • escalate/bind verbs: Meta-permissions that allow modifying or binding Roles/ClusterRoles

OPA Gatekeeper Architecture

OPA Gatekeeper operates as a Kubernetes admission webhook, performing policy validation before the API Server creates, modifies, or deletes resources.

Components

  1. Gatekeeper Controller Manager: The core component that processes admission webhook requests and evaluates Rego policies
  2. Audit Controller: Periodically inspects already-deployed resources for policy compliance
  3. ConstraintTemplate CRD: Defines the Rego policy logic and parameter schema as a template
  4. Constraint CRD: Instantiates a ConstraintTemplate, specifying concrete policy targets and parameters

Installation

# Install Gatekeeper (Helm)
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update

helm install gatekeeper gatekeeper/gatekeeper \
  --namespace gatekeeper-system \
  --create-namespace \
  --set audit.interval=60 \
  --set constraintViolationsLimit=50 \
  --set audit.fromCache=true

# Verify installation
kubectl get pods -n gatekeeper-system
kubectl get crd | grep gatekeeper

Request Flow

The Gatekeeper policy evaluation flow works as follows:

  1. A user or controller sends a resource create/modify request to the API Server
  2. The API Server performs authentication (AuthN) and authorization (AuthZ/RBAC)
  3. During the admission phase, the request is forwarded to the Gatekeeper webhook
  4. Gatekeeper finds matching Constraints for the resource and evaluates Rego policies
  5. If violations are found, the request is denied with violation messages
  6. If no violations exist, the request is allowed and persisted to etcd

ConstraintTemplate and Rego Policy Authoring

Basic Structure

A ConstraintTemplate consists of two parts: the CRD spec (parameter schema) and the Rego code (policy logic).

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels:
              type: array
              items:
                type: string
              description: 'List of required labels'
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels

        violation[{"msg": msg, "details": {"missing_labels": missing}}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("Resource is missing required labels: %v", [missing])
        }

Applying a Constraint

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-team-label
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: ['']
        kinds: ['Namespace']
    excludedNamespaces:
      - kube-system
      - gatekeeper-system
  parameters:
    labels:
      - 'team'
      - 'cost-center'

Real-World Policy Use Cases

Use Case 1: Image Registry Restriction

Enforce that containers in production clusters can only pull images from approved registries.

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedrepos
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRepos
      validation:
        openAPIV3Schema:
          type: object
          properties:
            repos:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedrepos

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not image_from_allowed(container.image)
          msg := sprintf("Container '%v' uses image '%v' from an unauthorized registry. Allowed registries: %v",
            [container.name, container.image, input.parameters.repos])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.initContainers[_]
          not image_from_allowed(container.image)
          msg := sprintf("Init container '%v' uses image '%v' from an unauthorized registry.",
            [container.name, container.image])
        }

        image_from_allowed(image) {
          repo := input.parameters.repos[_]
          startswith(image, repo)
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: prod-allowed-repos
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: ['']
        kinds: ['Pod']
    namespaces:
      - production
      - staging
  parameters:
    repos:
      - 'gcr.io/my-company/'
      - 'us-docker.pkg.dev/my-company/'
      - 'registry.internal.company.com/'

Use Case 2: Mandatory Resource Requests and Limits

Require all containers to have CPU and memory requests/limits configured.

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequireresourcelimits
spec:
  crd:
    spec:
      names:
        kind: K8sRequireResourceLimits
      validation:
        openAPIV3Schema:
          type: object
          properties:
            requiredResources:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequireresourcelimits

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          resource := input.parameters.requiredResources[_]
          not container.resources.limits[resource]
          msg := sprintf("Container '%v' is missing resources.limits.%v", [container.name, resource])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          resource := input.parameters.requiredResources[_]
          not container.resources.requests[resource]
          msg := sprintf("Container '%v' is missing resources.requests.%v", [container.name, resource])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireResourceLimits
metadata:
  name: require-cpu-memory-limits
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: ['']
        kinds: ['Pod']
    excludedNamespaces:
      - kube-system
      - gatekeeper-system
  parameters:
    requiredResources:
      - 'cpu'
      - 'memory'

Use Case 3: Block Privileged Containers

Prevent containers from running in privileged mode.

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sdisallowprivileged
spec:
  crd:
    spec:
      names:
        kind: K8sDisallowPrivileged
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sdisallowprivileged

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          container.securityContext.privileged == true
          msg := sprintf("Privileged containers are not allowed: '%v'", [container.name])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.initContainers[_]
          container.securityContext.privileged == true
          msg := sprintf("Privileged init containers are not allowed: '%v'", [container.name])
        }

RBAC Auditing and Monitoring

kubectl-Based Auditing

# Check current user permissions
kubectl auth can-i --list

# Check a specific ServiceAccount's namespace permissions
kubectl auth can-i --list --as=system:serviceaccount:production:app-deployer -n production

# Verify a specific action
kubectl auth can-i delete pods --as=system:serviceaccount:staging:ci-runner -n staging

# Find subjects that can read secrets cluster-wide (kubectl-who-can plugin)
kubectl who-can get secrets --all-namespaces

# Discover ClusterRoleBindings with excessive permissions
kubectl get clusterrolebindings -o json | \
  jq '.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name, .subjects'

Gatekeeper Audit Capabilities

Gatekeeper's Audit feature inspects already-deployed resources for policy violations. Setting enforcementAction: dryrun records violations without blocking requests.

# View policy violation status
kubectl get k8sallowedrepos prod-allowed-repos -o yaml | \
  grep -A 100 "status:" | head -50

# Summary of all Constraint violations
kubectl get constraints -o json | \
  jq '.items[] | {name: .metadata.name, kind: .kind, violations: (.status.totalViolations // 0)}'

Monitoring Integration

Gatekeeper exposes Prometheus metrics by default:

  • gatekeeper_violations: Number of violations found during the current audit
  • gatekeeper_request_duration_seconds: Admission request processing time
  • gatekeeper_request_count: Total admission requests (by allow/deny)
  • gatekeeper_constraint_template_status: ConstraintTemplate status

Comparison: RBAC vs OPA Gatekeeper vs PSA vs Kyverno

A comprehensive comparison of Kubernetes access control and policy engines.

CategoryRBACOPA GatekeeperPod Security Admission (PSA)Kyverno
LayerAuthorizationAdmissionAdmissionAdmission
Policy LanguageDeclarative YAMLRegoBuilt-in profiles (Privileged/Baseline/Restricted)Declarative YAML
Learning CurveLowHigh (requires Rego)Very LowLow
FlexibilityLow (grant/deny only)Very HighLow (only 3 profiles)High
Mutation SupportN/ASupported (Assign/Modify)Not supportedSupported (mutate)
Resource GenerationN/ANot supportedNot supportedSupported (generate)
AuditRequires audit log analysisBuilt-in Audit featureaudit/warn modesPolicyReport CRD
CNCF StageBuilt into KubernetesGraduatedBuilt into KubernetesIncubating
Resource ConsumptionNone (built into API Server)High (multiple Pods)Very LowMedium
Recommended ForBase access controlComplex policy logicPod security baseline enforcementKubernetes-native policies

Recommended combination: Using RBAC (base authorization) + PSA (Pod security baseline) + Gatekeeper or Kyverno (custom policies) together is the best practice for production environments. Kyverno is well-suited for simple policies, while OPA Gatekeeper excels at complex cross-resource validation.

Failure Cases and Recovery Procedures

Case 1: Gatekeeper Webhook Failure Blocking All Deployments

Symptom: All Pod creations are rejected with error messages showing webhook "validation.gatekeeper.sh" denied the request or connection timeouts.

Root Cause: Gatekeeper Controller Pods have crashed or are unresponsive due to resource exhaustion.

Recovery Procedure:

# 1. Check Gatekeeper Pod status
kubectl get pods -n gatekeeper-system

# 2. Emergency: Temporarily disable webhook (change failurePolicy to Ignore)
kubectl get validatingwebhookconfigurations gatekeeper-validating-webhook-configuration -o yaml > webhook-backup.yaml
kubectl patch validatingwebhookconfigurations gatekeeper-validating-webhook-configuration \
  --type='json' -p='[{"op": "replace", "path": "/webhooks/0/failurePolicy", "value": "Ignore"}]'

# 3. Restart Gatekeeper Pods
kubectl rollout restart deployment gatekeeper-controller-manager -n gatekeeper-system

# 4. After confirming recovery, restore failurePolicy
kubectl apply -f webhook-backup.yaml

Prevention: Changing Gatekeeper's failurePolicy from Fail to Ignore improves availability but allows policy bypass. In production, maintain Fail while ensuring adequate resources and replicas for Gatekeeper Pods.

Case 2: Permission Leakage Through Excessive ClusterRoleBindings

Symptom: All ServiceAccounts can unexpectedly read cluster resources.

Diagnosis and Recovery:

# Find subjects bound to cluster-admin
kubectl get clusterrolebindings -o json | \
  jq '.items[] | select(.roleRef.name=="cluster-admin") | {name: .metadata.name, subjects: .subjects}'

# Remove unnecessary ClusterRoleBindings
kubectl delete clusterrolebinding suspicious-admin-binding

# Audit all ClusterRoleBindings
kubectl get clusterrolebindings -o json | \
  jq '.items[] | {name: .metadata.name, role: .roleRef.name, subjects: [.subjects[]? | .kind + ":" + .name]}'

Case 3: ConstraintTemplate Syntax Error Causing Policy Inaction

Symptom: A Constraint has been created but the policy is not being enforced. The STATUS column in kubectl get constrainttemplates shows an abnormal state.

Diagnosis:

# Check ConstraintTemplate status
kubectl get constrainttemplate k8srequiredlabels -o json | jq '.status'

# Check for Rego syntax errors
kubectl describe constrainttemplate k8srequiredlabels | grep -A 10 "Status:"

# Check Gatekeeper logs for errors
kubectl logs -n gatekeeper-system -l control-plane=controller-manager --tail=100

Operational Checklist

A checklist for auditing access control in production Kubernetes clusters.

RBAC Checklist

  • Are there any unnecessary users in the system:masters group?
  • Have all subjects bound to the cluster-admin ClusterRole been identified?
  • Do any Roles/ClusterRoles use wildcard (*) permissions?
  • Is automountServiceAccountToken: false set as the default for ServiceAccounts?
  • Are appropriate Roles/RoleBindings configured per namespace?
  • Are sensitive permissions (pods/exec, secrets, rolebindings) granted minimally?
  • Have unused ServiceAccounts and RoleBindings been cleaned up?
  • Are RBAC configurations managed in Git (GitOps)?

OPA Gatekeeper Checklist

  • Is the Gatekeeper Controller deployed with high availability (2+ replicas)?
  • Is failurePolicy configured appropriately for production requirements?
  • Are system namespaces (kube-system, gatekeeper-system) properly excluded?
  • Is there a process to test new policies in dryrun mode first?
  • Are Audit results reviewed regularly?
  • Does the ConstraintTemplate Rego code pass unit tests?
  • Are Gatekeeper metrics integrated with Prometheus/Grafana?
  • Is there a documented emergency recovery runbook for webhook failures?

Periodic Audit Items

  • Quarterly RBAC permission review: Identify subjects with excessive permissions
  • Monthly Gatekeeper Audit report: Current policy violation resource status
  • ServiceAccount token usage pattern analysis: Clean up unused tokens
  • Update RBAC and Gatekeeper policies when new CRDs/APIs are introduced

Conclusion

Kubernetes access control is not complete with RBAC alone. RBAC answers "who should be granted which permissions," but validating "whether authorized actions comply with policies" requires an admission controller like OPA Gatekeeper. Using both mechanisms together, enforcing baseline security with Pod Security Admission, and performing regular audits is how you realize the least-privilege principle in production environments.

The operational habits that prevent security incidents are managing policies as code (Policy as Code), always evaluating impact with dryrun mode before changes, and preparing emergency recovery procedures for failures in advance. RBAC and OPA Gatekeeper are technical tools, but effective operation requires integration with your organization's access control governance framework.