오픈소스

[Kyverno] Kyverno 이해하기

안녕유지 2025. 3. 12. 04:48

이번 포스팅은 쿠버네티스 정책 엔진 중 하나인 Kyverno에 대해 알아보겠습니다. 
 

스스로 테스트용으로 작성한 내용을 공개 전환하여 목차나 설명 과정에서 미흡한 부분이 있을 수 있습니다. 피드백은 언제나 환영입니다.!

 

Kyverno

 
Kyverno란 쿠버네티스 클러스터에서 정책 관리를 자동화 하기 위한 오픈 소스 정책 엔진
Kyverno를 사용하면 쿠버네티스의 오브젝트 생성, 업데이트 또는 삭제 시에 정책을 검증하고, 필요한 경우 자동으로 수정할 수 있습니다.
사용할 때 다음과 같은 장점을 가집니다.

1) Kubernetes Native Open Source
2) 특정 프로그래밍 언어를 배울 필요가 없이 정책을 생성할 수 있습니다. (Custom Resource)
3) 모든 정책은 선언적으로 yaml 문법에 의해 사용 및 관리됩니다.
 
https://kyverno.io/

Kyverno

Kyverno is a policy engine designed for Kubernetes

kyverno.io

 
 

정책엔진이 필요한 이유

  1. 보안 강화
    쿠버네티스 클러스터 내에서 컨테이너가 루트(root) 권한으로 실행되는 것을 방지하거나, 부적절하게 서명되거나 인증되지 않은 컨테이너 이미지의 실행을 방지하는 등 보안을 강화하기 위해 필요합니다.
  2. 협업 및 멀티 테넌시
    여러 팀 또는 사용자 간의 협업을 지원하고, 쿠버네티스 클러스터 내에서 다중 테넌시를 구현하기 위해 필요합니다.
  3. 비용 제어
    모든 서비스가 새로운 로드 밸런서를 할당하는 것을 막거나 CPU 및 메모리 요청을 확인하는 등 비용을 효율적으로 관리하기 위해 필요합니다.
  4. 리소스 유효성 검사
    컨테이너 및 다른 리소스의 요청이 유효한지 검사하여 클러스터의 성능과 안정성을 유지하기 위해 필요합니다.
  5. 정책 준수
    기업이나 조직의 정책에 따라 리소스의 생성, 수정, 삭제 등에 제약을 두어 정책 준수를 강제하기 위해 필요합니다.

 

Kyverno 작동 방식

Kyverno는 클러스터에서 Dynamic admission controller로 동작합니다.
Kubernetes API 서버로 부터 Mutating, Validation Admission Webhook HTTP 콜백을 수신하고, 일치하는 정책을 적용하여 정책을 수행하거나 요청을 거부합니다.
 

 
 

Admission Controller

  • Authentication: 누구인지 확인
  • Authorization: 권한 확인
  • Admission Control: 요청한 작업 허용할 것인지, API 요청 가로채 특정 행동을 검사(validate)하거나 변경(mutate)

Dynamic Admission Controller에 사용되는 플러그인
(standard : default로 제공, dynamic : 특정 유스케이스를 대응하기 위함)

  • MutatingAdmissionWebhook : 요청받은 객체를 수정
  • VadliatingAdmissionWebhook : 요청을 승인하거나 거절

-> 해당 과정이 모두 완료 되어야 쿠버네티스 오브젝트가 생성되고 etcd에 저장됩니다.
 

웹훅 사용 조건

Admisstion Controller가 활성화가 되어있어야 합니다.
API에 admissionregistration.k8s.io/v1가 활성화 되어있어야 합니다.
쿠버네티스 요청은 AdmissionReview 구조로 처리해야합니다.
 

웹훅 구성

1. rules

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
...
webhooks:
- name: my-webhook.example.com
  rules:
  - operations: ["CREATE", "UPDATE"]
    apiGroups: ["apps"]
    apiVersions: ["v1", "v1beta1"]
    resources: ["deployments", "replicasets"] /// 위 요청에 해당하는 리소스
    scope: "Namespaced"
  ...

 
2. objectSelector

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
webhooks:
- name: my-webhook.example.com
  objectSelector: /// 개체 중 Selector와 일치하는 경우
    matchLabels:
      foo: bar
  rules:
  - operations: ["CREATE"]

 
3. namespaceSelector

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
webhooks:
  - name: my-webhook.example.com
    namespaceSelector: /// 네임스페이스 중 Selector와 일치하는 경우
      matchExpressions:
        - key: runlevel
          operator: NotIn
          values: ["0","1"]
    rules:
      - operations: ["CREATE"]

 
4. matchPolicy

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
webhooks:
- name: my-webhook.example.com
  matchPolicy: Equivalent // 정확할때만 요청 가로챌지, 아닐지
  rules:
  - operations: ["CREATE","UPDATE","DELETE"]

 
5. matchConditions (k8s v1.28+ beta)

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
webhooks:
  - name: my-webhook.example.com
    matchPolicy: Equivalent
    rules:
      - operations: ['CREATE','UPDATE']
        apiGroups: ['*']
        apiVersions: ['*']
        resources: ['*']
    failurePolicy: 'Ignore' # Fail-open (optional)
    sideEffects: None
    clientConfig:
      service:
        namespace: my-namespace
        name: my-webhook
      caBundle: '<omitted>'
    /// 필터링 추가하고 싶을때 모든 조건이 true일 경우
    matchConditions:
      - name: 'exclude-leases' # Each match condition must have a unique name
        expression: '!(request.resource.group == "coordination.k8s.io" && request.resource.resource == "leases")' # Match non-lease resources.
      - name: 'exclude-kubelet-requests'
        expression: '!("system:nodes" in request.userInfo.groups)' # Match requests made by non-node users.
      - name: 'rbac' # Skip RBAC requests, which are handled by the second webhook.
        expression: 'request.resource.group != "rbac.authorization.k8s.io"'

 

Kyverno Architecture

 
Kyverno 정책은 리소스의 종류, 이름, 라벨 셀렉터 등 다양한 것과 매칭할 수 있습니다.
Kyverno는 쿠버네티스 클러스터에 설치되어 동작한다. 클러스터 내에서 새로운 리소스가 생성되거나 수정될 때, API 서버는 이를 감지하고 관련된 웹훅을 호출한다. 이 웹훅은 Kyverno에게 해당 리소스에 대한 정책을 적용할 수 있는 기회를 제공합니다.
 

  • Admission Webhooks
    • API 서버가 새로운 리소스를 생성 또는 수정할 때 호출되는 되며, API 서버에 들어오는 요청을 처리를 위해 Engine으로 보냄
  • Kyverno Engine
    • API 서버로부터 전달받은 리소스에 대해 정책을 평가하고 결정한다. 이를 통해 리소스가 수용되거나 거부된다. 배포된 오브젝트는 아닌, Kyverno 바이너리에 내장되어있음
  • Webhook Controller
    • 미리 만들어진 정책을 기반으로 웹훅의 동작을 조정하고, 웹훅은 정책을 실제로 클러스터에 적용
  • Cert Renewer
    • 웹훅에 필요한 쿠버네티스 Secret으로 저장된 인증서를 감시하고 갱신
  • Report Controller
    • 클러스터 내에서 Policy 리포트를 유지하고 관리
  • Background Controller
    • 정책 생성 및 기존 리소스의 변형을 담당

 

Kyverno와 일반적인 Admission Controller 차이

1. Kyverno의 정책 기반 접근

Kyverno는 정책을 사용하여 클러스터 리소스를 검증하고 수정하는 데 중점을 둡니다.
리소스의 생성, 수정, 삭제 등과 관련된 작업을 검사하고 원하는 정책을 적용하여 수행합니다. 
일반적인 Admission Controller는 주로 리소스가 클러스터에 제출되는 시점에서 특정 작업을 수행한다.

2. 정책 표현 방식

Kyverno는 YAML 또는 JSON 형식의 정책을 사용하여 리소스를 검증하고 수정합니다.
일반적인 Admission Controller는 보다 복잡한 프로그래밍 방식을 사용하여 작동합니다.

3. 동적 정책 관리

Kyverno는 동적으로 정책을 관리하고 적용할 수 있다. 클러스터 내에서 변경되는 요구 사항에 따라 정책을 업데이트하고 적용할 수 있습니다.
일반적인 Admission Controller는 보통 정적으로 구성되며, 변경되는 요구 사항에 대응하기 위해 수동으로 업데이트해야 합니다.
 
 

Kyverno를 사용한 유효성 검사 정책 예제

ClusterPolicy 예제 매니페스트

kubectl create -f- << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
spec:
  validationFailureAction: Enforce
  rules:
  - name: check-team
    match:
      any:
      - resources:
          kinds:
          - Pod
    validate:
      message: "label 'team' is required"
      pattern:
        metadata:
          labels:
            team: "?*"

 
위의 하나의 Rule은 match/exclude 설정, validate, mutate, generate, verify images 중 하나의 정책이 적용되고, Admission Control 규칙에 의해 Mutation 규칙은 validation 규칙 전에 적용됩니다.
 

설치 및 데모

설치

  1. 매니페스트로 배포
  2. helm으로 배포 <- 데모 버전과 프로덕션 버전 차이는 고가용성 차이로 helm 설치로 결정

호환성 : 현재 Docker Desktop으로 설치된 로컬 쿠버네티스 버전이 1.25 버전이라, 1.11 버전으로 설치 (min kubernetes version)
고가용성 : Kyverno는 각각 별도의 Kubernetes 배포에 포함된 다양한 컨트롤러로 구성되어 컨트롤러별로 고가용성 유지 시 추가 복제본 필요
 

❯ helm install kyverno-policies kyverno/kyverno-policies -n kyverno --version 3.1.4         
NAME: kyverno-policies
LAST DEPLOYED: Wed Mar 13 03:36:55 2025
NAMESPACE: kyverno
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Thank you for installing kyverno-policies 3.1.4 😀

We have installed the "baseline" profile of Pod Security Standards and set them in Audit mode.

Visit https://kyverno.io/policies/ to find more sample policies.

 

1) Validation

호출된 라벨이 모든 파드에 존재하는 지 확인하는 Validation 정책
 
ClusterPolicy 생성

❯ kubectl create -f- << EOF                                                 
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
spec:
  validationFailureAction: Enforce # 정책 위하반 리소스 생성 수정 거부, (<-> audit으로 설정 시 생성 및 수정은 되나 규칙 위한으로 기록되어 정책 리포트에 기록됨)
  rules:
  - name: check-team
    match:
      any:
      - resources:
          kinds:
          - Pod
    validate:
      message: "label 'team' is required"
      pattern:
        metadata:
          labels:
            team: "?*"
EOF
clusterpolicy.kyverno.io/require-labels created

 
Policy에 맞지 않는 오브젝트 생성
(rule auto-generation을 통해 deployment로 배포한 pod도 막습니다.)

❯ kubectl create deployment nginx --image=nginx                              
error: failed to create deployment: admission webhook "validate.kyverno.svc-fail" denied the request:

resource Deployment/default/nginx was blocked due to the following policies

require-labels:
  autogen-check-team: 'validation error: label ''team'' is required. rule autogen-check-team
    failed at path /spec/template/metadata/labels/team/'

 
kyverno admission controller log

I0218 18:36:55.912619       1 controller.go:45] "resource added" logger="setup.cluster-policy" type="ClusterPolicy" name="disallow-selinux"
I0218 18:36:56.020055       1 run.go:115] "Retrying request" logger="webhook-controller.worker" id=0 obj="kyverno-resource-validating-webhook-cfg" error="Operation cannot be fulfilled on clusterpolicies.kyverno.io \"disallow-host-process\": the object has been modified; please apply your changes to the latest version and try again"
I0218 18:36:56.206910       1 run.go:115] "Retrying request" logger="webhook-controller.worker" id=1 obj="kyverno-resource-mutating-webhook-cfg" error="Operation cannot be fulfilled on clusterpolicies.kyverno.io \"restrict-sysctls\": the object has been modified; please apply your changes to the latest version and try again"
I0218 18:36:56.317969       1 run.go:115] "Retrying request" logger="webhook-controller.worker" id=1 obj="kyverno-resource-mutating-webhook-cfg" error="Operation cannot be fulfilled on clusterpolicies.kyverno.io \"disallow-selinux\": the object has been modified; please apply your changes to the latest version and try again"
I0218 18:38:15.116851       1 controller.go:45] "resource added" logger="setup.cluster-policy" type="ClusterPolicy" name="require-labels"
I0218 18:38:28.832439       1 validation.go:103] "validation failed" logger="webhooks.resource.validate" gvk="apps/v1, Kind=Deployment" gvr={"group":"apps","version":"v1","resource":"deployments"} namespace="default" name="nginx" operation="CREATE" uid="3a4c3e8e-d437-4f00-81ec-7d7addb4fc50" user={"username":"docker-for-desktop","groups":["system:masters","system:authenticated"]} roles=null clusterroles=["cluster-admin","system:basic-user","system:discovery","system:public-info-viewer"] resource.gvk="apps/v1, Kind=Deployment" kind="Deployment" resource="default/Deployment/nginx" operation="CREATE" gvk="apps/v1, Kind=Deployment" action="Enforce" policy="require-labels" failed rules=["autogen-check-team"]
I0218 18:38:28.832519       1 block.go:29] "blocking admission request" logger="webhooks.resource.validate" gvk="apps/v1, Kind=Deployment" gvr={"group":"apps","version":"v1","resource":"deployments"} namespace="default" name="nginx" operation="CREATE" uid="3a4c3e8e-d437-4f00-81ec-7d7addb4fc50" user={"username":"docker-for-desktop","groups":["system:masters","system:authenticated"]} roles=null clusterroles=["cluster-admin","system:basic-user","system:discovery","system:public-info-viewer"] resource.gvk="apps/v1, Kind=Deployment" kind="Deployment" action="validate" resource="default/Deployment/nginx" operation="CREATE" gvk="apps/v1, Kind=Deployment" policy="require-labels"
I0218 18:38:28.833565       1 handlers.go:131] "admission request denied" logger="webhooks.resource.validate" gvk="apps/v1, Kind=Deployment" gvr={"group":"apps","version":"v1","resource":"deployments"} namespace="default" name="nginx" operation="CREATE" uid="3a4c3e8e-d437-4f00-81ec-7d7addb4fc50" user={"username":"docker-for-desktop","groups":["system:masters","system:authenticated"]} roles=null clusterroles=["cluster-admin","system:basic-user","system:discovery","system:public-info-viewer"] resource.gvk="apps/v1, Kind=Deployment" kind="Deployment"
I0218 18:38:28.834043       1 event_broadcaster.go:338] "Event occurred" object="require-labels" kind="ClusterPolicy" apiVersion="kyverno.io/v1" type="Warning" reason="PolicyViolation" action="Resource Blocked" note="Deployment default/nginx: [autogen-check-team] fail (blocked); validation error: label 'team' is required. rule autogen-check-team failed at path /spec/template/metadata/labels/team/"

 
정책에 맞는 pod 생성

❯ kubectl run nginx --image nginx --labels team=backend                       
pod/nginx created

 
policyreport 확인

❯ kubectl get policyreport -o wide                                           
NAME                                   KIND   NAME    PASS   FAIL   WARN   ERROR   SKIP   AGE
5e9f03fa-d4f9-4e5b-827e-f4c933269dde   Pod    nginx   13     0      0      0       0      21s

❯ kubectl describe policyreport 5e9f03fa-d4f9-4e5b-827e-f4c933269dde          
Name:         5e9f03fa-d4f9-4e5b-827e-f4c933269dde
Namespace:    default
Labels:       app.kubernetes.io/managed-by=kyverno
Annotations:  <none>
API Version:  wgpolicyk8s.io/v1alpha2
Kind:         PolicyReport
Metadata:
  Creation Timestamp:  2025-03-13T18:44:45Z
  Generation:          2
  Managed Fields:
    API Version:  wgpolicyk8s.io/v1alpha2
    Fields Type:  FieldsV1

 

2) Mutate

ClusterPolicy 생성
새로운 파드에 라벨이 할당되지 않았다면 team이라는 라벨에 bravo라는 value를 추가합니다.

❯ kubectl create -f- << EOF                                                   
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-labels
spec:
  rules:
  - name: add-team
    match:
      any:
      - resources:
          kinds:
          - Pod
    mutate:
      patchStrategicMerge:
        metadata:
          labels:
            +(team): bravo
EOF
clusterpolicy.kyverno.io/add-labels created

 
라벨이 붙지 않은 pod 생성시 위 Mutate policy로 라벨이 추가되었습니다.

❯ kubectl run redis --image redis                                             
pod/redis created

❯ kubectl get pod redis --show-labels                                       
NAME    READY   STATUS    RESTARTS   AGE   LABELS
redis   1/1     Running   0          10s   run=redis,team=bravo

 
라벨이 붙은 pod 생성시 위 Mutate policy로 라벨이 추가되지 않았습니다.

❯ kubectl run newredis --image redis -l team=alpha                            
pod/newredis created
                                                                                        
❯ kubectl get pod newredis --show-labels                                      
NAME       READY   STATUS    RESTARTS   AGE   LABELS
newredis   1/1     Running   0          5s    team=alpha

 

3) Generation

Kyverno에는 정책에 저장된 정의를 기반으로 새로운 Kubernetes 리소스를 생성하는 기능
Kyverno 생성 정책을 사용하여 새 네임스페이스에 이미지 풀 비밀을 생성합니다.

❯ kubectl -n default create secret docker-registry regcred \                  
  --docker-server=myinternalreg.corp.com \
  --docker-username=john.doe \
  --docker-password=Passw0rd123! \
  --docker-email=john.doe@corp.com
secret/regcred created

 
Kyverno 정책을 만든다. 새로운 네임스페이스와 일치하며, 이전에 생성한 secret을 새 네임스페이스에 복제합니다.

❯ kubectl create -f- << EOF                                                   
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: sync-secrets
spec:
  rules:
  - name: sync-image-pull-secret
    match:
      any:
      - resources:
          kinds:
          - Namespace
    generate:
      apiVersion: v1
      kind: Secret
      name: regcred
      namespace: "{{request.object.metadata.name}}"
      synchronize: true
      clone:
        namespace: default
        name: regcred
EOF
clusterpolicy.kyverno.io/sync-secrets created

 
새 네임스페이스를 생성합니다.

❯ kubectl create ns mytestns                                                  
namespace/mytestns created

 
새로운 네임스페이스에서 secret을 가져와서 regcred가 존재하는지 확인합니다.

❯ kubectl -n mytestns get secret                                              
NAME      TYPE                             DATA   AGE
regcred   kubernetes.io/dockerconfigjson   1      45s

 

비고

1) 로컬 환경에서 kube-apiserver 설정 수정
docker-desktop, minikube에서 kube-apiserver의 admission-controller를 수정하려고 하였으나 (https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#how-do-i-turn-on-an-admission-controller) 수정이 불가하거나, 삭제 후 수정하여 새로 시작해야했습니다.
 
2) kyverno 정책 적용시 반영사항 확인
https://playground.kyverno.io/#/
Policy 등을 생성할 때 JMES를 구문을 사용하는데, 웹에서 Policy를 적용하면 오브젝트에 적용이 되는 것을 미리 확인해볼 수 있습니다.