Certified Kubernetes Security Specialist (CKS) #16 Admission control: OPA/Gatekeeper, Kyverno

#15 이미지 서명: cosign, SBOM에서는 이미지에 서명을 붙이고 그 서명을 검증하는 흐름을 익혔습니다. 그런데 서명되지 않은 이미지, latest 태그를 쓴 Pod, root로 도는 컨테이너를 클러스터가 받아들이기 전에 막으려면 요청을 가로채 검사하는 관문이 필요합니다. 이번 글은 그 관문인 admission control을 OPA/Gatekeeper와 Kyverno 두 도구로 다루겠습니다.

CKS 커리큘럼에서 admission control은 Monitoring,Logging,Runtime Security도메인에 매핑되지만, Falco가 런타임에서 일어난 일을 탐지하는 도구라면 admission control은 위험한 워크로드가 애초에 클러스터에 들어오지 못하게 막는 정책 강제 도구입니다. 탐지가 아니라 예방이라는 점에서 성격이 다릅니다.

admission controller란 무엇인가 #

쿠버네티스 API 서버는 들어온 요청을 처리할 때 여러 단계를 거칩니다. 인증(authentication)으로 누구인지 확인하고, 인가(authorization)로 권한이 있는지 확인한 뒤, admission control 단계에서 그 요청을 실제로 받아들일지 한 번 더 검사합니다. 즉 admission controller는 요청이 etcd에 저장되기 직전에 끼어들어 요청을 들여다보는 관문입니다.

쿠버네티스에는 NamespaceLifecycle, LimitRanger 같은 빌트인 admission controller가 컴파일되어 들어 있습니다. 하지만 사용자가 정의한 임의의 정책을 강제하려면 외부로 요청을 보내 판정을 받는 방식이 필요한데, 이것이 admission webhook입니다.

validating vs mutating webhook #

admission webhook은 동작에 따라 두 종류로 나뉩니다. 이 구분이 시험에서 자주 묻는 핵심입니다.

종류하는 일예시
ValidatingAdmissionWebhook요청을 검증만 함. 통과 또는 거부latest 태그를 쓴 Pod를 거부
MutatingAdmissionWebhook요청 객체를 변형함. 필드 추가,수정모든 Pod에 라벨을 자동 주입

처리 순서도 정해져 있습니다. API 서버는 mutating webhook을 먼저 호출해 객체를 변형한 다음, validating webhook을 호출해 최종 객체를 검증합니다. 변형이 먼저이고 검증이 나중인 이유는, 변형으로 바뀐 결과를 검증이 다시 확인해야 정책의 일관성이 깨지지 않기 때문입니다.

이 두 webhook은 단지 “외부로 요청을 보낸다"는 골격일 뿐, 무엇을 검증하고 무엇을 변형할지는 정책 엔진이 결정합니다. 그 정책 엔진의 대표가 OPA/Gatekeeper와 Kyverno입니다.

OPA/Gatekeeper #

OPA(Open Policy Agent)는 범용 정책 엔진이고, Gatekeeper는 그 OPA를 쿠버네티스 admission webhook으로 통합한 컴포넌트입니다. Gatekeeper를 설치하면 ValidatingAdmissionWebhook이 등록되어, 들어오는 요청을 OPA 정책으로 판정합니다.

Rego와 두 단계 리소스 #

Gatekeeper의 정책은 Rego라는 선언형 질의 언어로 작성합니다. 그리고 정책을 두 개의 커스텀 리소스로 나눠 정의하는 것이 특징입니다.

  • ConstraintTemplate: 정책의 로직을 Rego로 정의하고, 그 정책을 강제할 새 커스텀 리소스 종류(kind)를 만듭니다.
  • Constraint: ConstraintTemplate이 만든 kind의 인스턴스로, 어떤 리소스에 어느 파라미터로 정책을 적용할지 지정합니다.

로직과 적용을 분리하므로, 한 번 만든 ConstraintTemplate을 파라미터만 바꿔 여러 Constraint로 재사용할 수 있습니다.

예제: 신뢰 레지스트리만 허용 #

먼저 ConstraintTemplate으로 “허용된 레지스트리로 시작하는 이미지만 받는다"는 로직을 정의하겠습니다.

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[_]
          satisfied := [good | repo := input.parameters.repos[_]; good := startswith(container.image, repo)]
          not any(satisfied)
          msg := sprintf("container <%v> uses an untrusted image <%v>", [container.name, container.image])
        }

다음으로 Constraint를 만들어 이 정책을 실제로 적용합니다. 여기서 어떤 레지스트리를 허용할지 파라미터로 넘깁니다.

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: only-trusted-registry
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    repos:
      - "registry.example.com/"

이제 registry.example.com/으로 시작하지 않는 이미지를 쓴 Pod를 만들려고 하면 admission 단계에서 거부됩니다. kubectl apply 결과에 violation 메시지로 지정한 문구가 그대로 나옵니다.

같은 방식으로 “특정 라벨이 없으면 거부"라는 정책도 만들 수 있습니다. Gatekeeper는 빌트인 ConstraintTemplate 라이브러리를 제공하므로, K8sRequiredLabels 같은 흔한 정책은 직접 Rego를 짜지 않고 라이브러리의 템플릿을 가져다 위와 같은 형태의 Constraint만 작성해도 됩니다. 위 Constraint에서 kind를 라이브러리 템플릿이 만든 kind로, parameters를 요구할 라벨 목록으로 바꾸면 그대로 동작합니다.

Kyverno #

Kyverno는 OPA/Gatekeeper의 대안으로, Rego 없이 쿠버네티스 YAML 문법만으로 정책을 작성하는 정책 엔진입니다. 별도의 질의 언어를 배울 필요가 없어 진입 장벽이 낮습니다. Kyverno도 admission webhook으로 동작하지만, validate뿐 아니라 mutate와 generate까지 한 도구로 처리합니다.

기능하는 일
validate규칙에 맞지 않는 요청을 거부하거나 경고
mutate요청 객체에 필드를 추가하거나 수정
generate리소스 생성 시 연관 리소스를 함께 생성

예제: latest 태그 금지 #

Kyverno의 정책은 ClusterPolicy(또는 네임스페이스 범위의 Policy)로 작성합니다. latest 태그를 쓴 컨테이너를 거부하는 정책은 다음과 같습니다.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-image-tag
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "이미지에는 명시적 태그가 있어야 하며 latest 태그는 금지됩니다"
        pattern:
          spec:
            containers:
              - image: "!*:latest"

validationFailureAction: Enforce가 위반 요청을 실제로 거부하게 만드는 키입니다. 이 값을 Audit으로 두면 거부 없이 위반만 기록합니다. pattern!*:latest는 “latest로 끝나지 않는 태그"라는 뜻으로, 태그 없는 이미지나 latest를 쓴 Pod를 모두 막습니다.

앞서 Gatekeeper로 만든 신뢰 레지스트리 정책도 Kyverno로 옮기면 ConstraintTemplate의 Rego 없이 같은 ClusterPolicy 구조에서 patternimage 값만 "registry.example.com/*"로 바꾸면 됩니다. Gatekeeper가 두 리소스와 Rego를 요구한 일을 Kyverno는 패턴 한 줄로 끝냅니다.

예제: 라벨 자동 주입 (mutate) #

mutate 정책은 요청을 거부하는 대신 객체를 고칩니다. 모든 Pod에 라벨을 자동으로 붙이는 정책은 다음과 같습니다.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-default-label
spec:
  rules:
    - name: add-team-label
      match:
        any:
          - resources:
              kinds:
                - Pod
      mutate:
        patchStrategicMerge:
          metadata:
            labels:
              team: platform

서명 검증 강제와의 연결 #

#15에서 cosign으로 이미지에 서명을 붙였습니다. 그 서명을 클러스터 차원에서 강제하는 관문이 바로 admission control입니다. Kyverno는 verifyImages 규칙으로 cosign 서명을 admission 단계에서 직접 검증할 수 있어, 서명되지 않았거나 신뢰하지 않는 키로 서명된 이미지를 거부합니다.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "registry.example.com/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKo...생략...
                      -----END PUBLIC KEY-----

이렇게 하면 #13~#15에서 만든 최소 이미지,스캔,서명의 흐름이 admission control이라는 마지막 관문에서 정책으로 강제됩니다. 공급망 보안의 결론이 정책 강제로 닫히는 구조입니다.

두 도구 비교 #

항목OPA/GatekeeperKyverno
정책 언어Rego(별도 학습 필요)쿠버네티스 YAML
정책 리소스ConstraintTemplate + Constraint(2단계)ClusterPolicy / Policy(1개)
validate가능가능
mutate가능(Assign 등)가능(patchStrategicMerge 등)
generate불가가능
이미지 서명 검증외부 연동 필요verifyImages로 내장
진입 장벽높음(Rego)낮음
표현력매우 높음(범용 정책)쿠버네티스 한정이지만 충분

요약하면 Rego의 표현력이 필요한 복잡한 정책이나 쿠버네티스 밖까지 정책을 공유해야 하는 환경이면 Gatekeeper, 쿠버네티스 안에서 빠르고 쉽게 정책을 강제하고 싶으면 Kyverno입니다. 시험에서는 어느 쪽이 주어질지 모르므로 두 도구의 기본 구조를 모두 알아 두는 것이 안전합니다.

시험 포인트 #

  • validating과 mutating의 차이와 순서를 정확히 구분합니다. mutating이 먼저, validating이 나중입니다. 변형 결과를 검증이 다시 확인하는 순서입니다.
  • Gatekeeper는 ConstraintTemplate으로 로직을, Constraint로 적용을 정의하는 2단계 구조입니다. 둘 중 하나만 만들면 정책이 동작하지 않습니다.
  • Kyverno는 ClusterPolicy 하나로 끝나며, validationFailureAction: Enforce가 위반을 실제로 거부하게 하는 키입니다. Audit은 기록만 합니다.
  • 시험 단골은 주어진 위반 매니페스트가 거부되는지 확인하는 작업입니다. 정책을 적용한 뒤 latest 태그나 신뢰하지 않는 레지스트리를 쓴 Pod를 kubectl apply로 만들어 보고, 거부 메시지가 나오는지 직접 확인하겠습니다.
  • 정책이 이미 배포된 상태에서 누락된 리소스를 찾아 채우는 문제도 나옵니다. ConstraintTemplate만 있고 Constraint가 없으면 정책이 적용되지 않으니, 이 구조를 떠올려 빠진 쪽을 만듭니다.
  • 정책 작성 문법은 공식 문서 열람이 허용되므로, Gatekeeper와 Kyverno 문서의 예제 위치를 미리 익혀 두면 작성 시간을 아낍니다.

정리 #

이번 글에서 잡은 것:

  • admission controller는 요청이 etcd에 저장되기 전 가로채 검사하는 관문입니다. validating은 검증, mutating은 변형이며 mutating이 먼저 동작합니다.
  • OPA/Gatekeeper는 Rego 정책을 ConstraintTemplate과 Constraint 2단계로 나눠 정의합니다. 로직과 적용의 분리로 재사용이 쉽습니다.
  • Kyverno는 Rego 없이 쿠버네티스 YAML만으로 validate,mutate,generate를 모두 처리합니다. 진입 장벽이 낮고 cosign 서명 검증을 내장합니다.
  • latest 태그 금지와 신뢰 레지스트리 제한을 두 도구로 각각 구현했고, 위반 매니페스트가 거부되는지 확인하는 시험 단골 패턴을 정리했습니다.

admission control은 #13~#15에서 다룬 공급망 보안을 클러스터 차원에서 강제하는 마지막 관문입니다. 정책 엔진의 더 넓은 활용은 쿠버네티스 심화 #3에서 다룬 클러스터 운영 맥락과도 이어집니다.

다음: Falco #

여기까지가 예방, 즉 위험한 워크로드를 들이지 않는 정책 강제였습니다. 그래도 클러스터 안에서 일어나는 이상 행동은 실시간으로 탐지해야 합니다.

#17 Falco 행동 분석, audit logs에서는 런타임에서 셸 실행,민감 파일 접근,예상 밖 네트워크 연결 같은 이상 행동을 규칙으로 탐지하는 Falco의 동작 원리와 규칙 작성법, 그리고 쿠버네티스 audit log를 설정하고 분석하는 법까지 직접 다뤄 보며 정리하겠습니다.

X