Kyverno ile Kubernetes Admission Webhook: Her Namespace'e Default-Deny, 3 Temel Kural ve Kyverno Kapanınca Açılan Kapı

Kyverno her namespace'e Cilium default-deny yazıyor, latest tag'i, default namespace'i ve limitsiz pod'u reddediyor. Sonra Kyverno'yu kapattım ve ölçtüm.

Yazan: Mirac Can Yılmaz 22 dk okuma
Kyverno ile Kubernetes Admission Webhook: Her Namespace'e Default-Deny, 3 Temel Kural ve Kyverno Kapanınca Açılan Kapı - Security yazısının kapak görseli

Bir Kubernetes cluster'ında "kurallarımız var" demek çoğu zaman bir wiki sayfası ve iyi niyet demek: image'a latest yazma, default namespace'e deploy etme, her container'a bellek sınırı koy, her namespace'e default-deny yaz. Kurallar insanların aklında durduğu sürece biri unutuyor ve unutulduğu gün hiçbir şey kırılmıyor; sorun, bir node belleksiz kaldığında ya da policy'siz bir namespace'ten beklenmedik bir bağlantı geçtiğinde ortaya çıkıyor.

Bu yazıda o kuralları cluster'ın kapısına, Kubernetes'in admission katmanına taşıyoruz. Kyverno her yeni namespace'e Cilium default-deny'ını kendisi yazıyor, kuralı çiğneyen pod'u daha API server'da reddediyor ve aynı kurallar PR aşamasında, cluster'a hiç dokunmadan CI'da çalışıyor. Lab'da Kyverno'nun resmi kütüphanesinden üç kural ekledim; ilk turda 12 ihlal buldular. Bunların ikisi, lab'ın "iyi örnek" diye yazdığı kendi manifestleriydi.

Yazıdaki bütün kodlar kyverno-cilium-policy-chain klasöründe.

Sorun ne, neden gerekli

Kubernetes, kendisine ne verilirse onu çalıştırıyor. latest tag'li bir image, bellek sınırı olmayan bir container, default namespace'e düşen bir test pod'u: hepsi geçerli YAML ve API server hepsini kabul ediyor. Code review bunların bir kısmını yakalıyor, ama kubectl apply'ı review'dan geçmeden çalıştıran herkes, her Helm chart'ı ve her operator bu kapının dışında kalıyor.

Ağ tarafında aynı boşluk daha sessiz. Cilium'da her namespace'e kapalı bir varsayılan (default-deny) koymak mümkün, ama biri onu elle yazmak zorunda. Yeni bir namespace açıldığında biri onu yazmayı unutursa, o namespace'teki her pod istediği yere konuşabiliyor ve bunu fark ettiren hiçbir hata yok, çünkü her şey çalışıyor.

Kurallar bir yerde zorlanmak zorunda. En ucuz yer, nesnenin cluster'a girdiği an: API server'ın her isteği kaydetmeden önce sorduğu admission katmanı. Orada reddedilen bir pod hiç çalışmıyor, düzeltmesi de yazan kişinin terminalinde, o anda yapılıyor.

Kubernetes admission webhook nedir: kapıda kim, ne soruyor

Her kubectl apply API server'a bir istek. API server isteği kimlik ve yetki kontrolünden sonra, etcd'ye yazmadan önce sırayla iki gruba soruyor: önce mutating admission (nesneyi değiştirebilir, örneğin eksik label ekler), sonra şema doğrulaması, sonra validating admission (sadece "evet" ya da "hayır" der). Bunların bir kısmı API server'ın içinde gömülü; dışarıdan eklenenler admission webhook: API server isteği HTTPS ile bir servise gönderiyor, cevaba göre devam ediyor ya da reddediyor. Kyverno bu servis.

Kubernetes dokümanında okunmaya değer sayfalar:

Kyverno'nun lab'da kurduğu webhook'lardan biri, cluster'dan olduğu gibi:

gpol.validate.kyverno.svc-ignore-finegrained-generate-default-deny   Ignore   10s
vpol.validate.kyverno.svc-fail-01f0a879                              Fail     10s
vpol.validate.kyverno.svc-fail-86e66184                              Fail     10s

Üç ayarın anlamı:

  • failurePolicy: Fail. Webhook cevap veremezse istek reddediliyor. Kyverno'nun servisini ulaşılamaz yapıp team-a'da pod açmayı denedim:

Error from server (InternalError): Internal error occurred: failed calling webhook "vpol.validate.kyverno.svc-fail-01f0a879": failed to call webhook: Post "https://kyverno-svc.kyverno.svc:443/vpol/disallow-host-network/disallow-latest-tag/disallow-privileged/require-app-label/require-requests-limits?timeout=10s": dial tcp 10.96.26.41:443: connect: no route to host

Ignore olsaydı pod policy'siz içeri girecekti. Generate webhook'u Ignore: namespace açmak Kyverno'ya bağlı değil (bunun bedeli Nerede patladı #3'te). - timeoutSeconds: 10. API server en fazla bu kadar bekliyor. Yavaş bir webhook her deploy'u yavaşlatıyor. - namespaceSelector. API server bazı namespace'lerin isteklerini webhook'a hiç göndermiyor. kube-system ve kyverno burada dışarıda; yoksa Kyverno düştüğünde kendi pod'unu bile yeniden yaratamaz.

Gerekenler

  • Docker (Mac'te Colima, 4 CPU / 8 GB). make up CNI'sız ve kube-proxy'siz bir kind cluster'ı kurup üstüne Cilium ve Hubble'ı ekliyor; başka bir hazırlık gerekmiyor.
  • helm, kubectl, kind 0.33, cilium CLI ve kyverno CLI (brew install kyverno cilium-cli).
  • Bu çalıştırmadaki sürümler: Kubernetes v1.37.0 (kind node), Cilium 1.20.2, Kyverno v1.19.1 (chart 3.9.1), Gateway API v1.6.1 CRD'leri, kyverno CLI 1.19.1. make up 128 saniye.

Neden Kyverno: Gatekeeper ve ValidatingAdmissionPolicy ile karşılaştırma

Admission'da policy için üç seçenek var ve üçü de production'da çalışıyor:

Kyverno OPA Gatekeeper ValidatingAdmissionPolicy (VAP)
Dil YAML + CEL (1.19'da CEL asıl dil) Rego CEL
Validate Var Var Var
Mutate Var Var (Assign CRD'leri) MutatingAdmissionPolicy
Generate (kaynak üretme) Var Yok Yok
Bileşen Deployment + webhook Deployment + webhook Yok, API server içinde
Raporlama PolicyReport CRD Audit CRD Yok (metrik + audit log)
Cluster'sız test kyverno apply gator test Yok

Kararı veren satır generate. "Namespace açılınca CiliumNetworkPolicy yarat" işini VAP da Gatekeeper da yapamıyor. VAP'ın avantajı bileşensiz olması; Kyverno 1.19'un yeni API'si de CEL kullandığı için kurallar ikisi arasında taşınabiliyor.

İkinci karar Kyverno'nun hangi API'si. Yıllarca herkes kyverno.io/v1 ClusterPolicy yazdı. 1.19 kurulurken Helm notu bunu açıkça söylüyor:

⚠️  WARNING: The legacy kyverno.io policy types are deprecated and will be removed in a future release. Migrate to their policies.kyverno.io replacements:
    - ClusterPolicy / Policy → ValidatingPolicy, MutatingPolicy, GeneratingPolicy, ImageValidatingPolicy (and their namespaced variants)

Bu yazıdaki dokuz policy yeni API'de (policies.kyverno.io/v1). İnternette bulacağınız ClusterPolicy örneklerinin alan adları farklı; karıştırmayın. Kyverno'nun policy kütüphanesi de yeni API'ye geçmiş (*-vpol, *-gpol, *-mpol klasörleri).

Üçüncü karar: önce Audit, sonra Deny. Audit modunda kural ihlali reddetmiyor, PolicyReport'a yazıyor. İlk günden Deny açsaydım lab'ın kendi "iyi" manifestleri ve kind'ın bir sistem bileşeni reddedilecekti; Adım 5'te gerçek listesi var. Tek istisna Gateway kuralı: bugün platform dışında Gateway yok, ilk günden Deny.

Adım 1 — Gateway CRD'leri, Cilium, Kyverno; bu sırayla

make up   # cluster → gateway-crds → cilium → kyverno

Sıra bir kez canımı yaktı (Nerede patladı #1): Kyverno başlarken cluster'daki kaynak türlerinin listesini alıyor. Gateway API CRD'leri ondan sonra kurulursa Gateway kuralı resource gateways not found diyerek her Gateway'i reddediyor. CRD'ler önce.

Kyverno'nun values dosyası sistem namespace'lerini iki ayrı yerde dışarıda tutuyor:

# kyverno/values.yaml (özü)
config:
  resourceFilters:
    - '[*/*,kube-system,*]'
    - '[*/*,kube-public,*]'
    - '[*/*,kube-node-lease,*]'
    - '[*/*,kyverno,*]'
    - '[*/*,local-path-storage,*]'
  webhooks:
    namespaceSelector:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: NotIn
          values: [kube-system, kyverno]

resourceFilters Kyverno'nun içinde çalışıyor: istek gelse de bu namespace'ler için policy değerlendirilmiyor. webhooks.namespaceSelector API server tarafında: bu namespace'lerin istekleri Kyverno'ya hiç gitmiyor. Kyverno ölüyken ikincisi kurtarıyor.

NAME                                             READY   STATUS    RESTARTS   AGE
kyverno-admission-controller-769b8f7647-7w9wq    1/1     Running   0          43s
kyverno-background-controller-86d8df7447-fkww8   1/1     Running   0          43s
kyverno-cleanup-controller-86c886ffff-8fs62      1/1     Running   0          43s
kyverno-reports-controller-57c7978d69-qx2wd      1/1     Running   0          43s

Dört controller, dört iş: admission webhook'a cevap veriyor, background generate işini yürütüyor, reports PolicyReport yazıyor, cleanup TTL'li kaynakları siliyor. Lab'da hepsi tek replica; Helm notu da uyarıyor: Setting the admission controller replica count below 2 means Kyverno is not running in high availability mode. Production'da admission controller 3.

Adım 2 — Kyverno GeneratingPolicy: her yeni namespace'e Cilium default-deny

Kyverno'nun varsayılan rolleri cilium.io grubunu tanımıyor. Kyverno bunun için aggregated ClusterRole kullanıyor: doğru label'ı taşıyan her ClusterRole, ilgili controller'ın rolüne kendiliğinden ekleniyor. Generate işini background controller yaptığı için yetki ona:

# rbac/kyverno-cilium-clusterrole.yaml (ilki)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kyverno:background-controller:cilium
  labels:
    rbac.kyverno.io/aggregate-to-background-controller: "true"
rules:
  - apiGroups: ["cilium.io"]
    resources: ["ciliumnetworkpolicies"]
    verbs: ["create", "update", "patch", "delete", "get", "list", "watch"]

Aynı dosyada iki rol daha var: admission controller'a cilium.io okuma, reports controller'a Gateway okuma (#2'de neden).

Policy. Tetikleyici Namespace üzerinde CREATE; üretilen şey her şeyi reddeden, sadece DNS'e izin veren bir CiliumNetworkPolicy, CEL içinde bir obje olarak:

# policies/generate-default-deny.yaml
apiVersion: policies.kyverno.io/v1
kind: GeneratingPolicy
metadata:
  name: generate-default-deny
spec:
  evaluation:
    synchronize:
      enabled: true          # policy changes propagate; a deleted CNP comes back
    generateExisting:
      enabled: true          # one-time pass over namespaces that already exist
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["namespaces"]
  matchConditions:
    - name: not-a-system-namespace
      expression: >-
        !(object.metadata.name in ['kube-system', 'kube-public', 'kube-node-lease',
          'kyverno', 'cilium-secrets', 'local-path-storage'])
    - name: not-opted-out
      expression: >-
        !(has(object.metadata.labels) &&
          'network.platform/default-deny' in object.metadata.labels &&
          object.metadata.labels['network.platform/default-deny'] == 'skip')
  variables:
    - name: nsName
      expression: "object.metadata.name"
    - name: downstream
      expression: >-
        [{
          "apiVersion": dyn("cilium.io/v2"),
          "kind": dyn("CiliumNetworkPolicy"),
          "metadata": dyn({
            "name": "default-deny",
            "namespace": string(variables.nsName),
            "labels": {"network.platform/managed-by": "kyverno"}
          }),
          "spec": dyn({
            "endpointSelector": {},
            "ingress": [],
            "egress": [
              {
                "toEndpoints": [
                  {"matchLabels": {"k8s:io.kubernetes.pod.namespace": "kube-system",
                                   "k8s:k8s-app": "kube-dns"}}
                ],
                "toPorts": [
                  {"ports": [{"port": "53", "protocol": "UDP"}, {"port": "53", "protocol": "TCP"}],
                   "rules": {"dns": [{"matchPattern": "*"}]}}
                ]
              }
            ]
          })
        }]
  generate:
    - expression: generator.Apply(variables.nsName, variables.downstream)
  • generateExisting: true. Policy kurulduğu anda var olan namespace'ler için bir kerelik tarama. Lab'da default namespace'i de bu sayede default-deny aldı.
  • synchronize: true. Policy değişirse üretilen CNP'ler güncelleniyor; biri CNP'yi silerse geri geliyor. Lab'da sildiğim CNP 5 saniyede geri geldi. Elle düzenlenen CNP ise geri yazılmadı (#6).
  • DNS istisnası CNP'nin içinde. Her şeyi kesen bir default-deny DNS'i de kesiyor ve pod'lar isim çözemiyor. İstisna üretilen her policy'nin içinde olduğu için kimse onu ayrıca yazmak zorunda değil.
make policies && make demo
namespace/team-a created
--- generated CiliumNetworkPolicy:
spec:
  description: 'Generated by Kyverno: deny all, allow DNS to kube-dns'
  endpointSelector: {}
  ingress: []
status:
  conditions:
  - message: Policy validation succeeded
    status: "True"
    type: Valid
$ kubectl get cnp -A
NAMESPACE   NAME           AGE     VALID
default     default-deny   4m10s   True
team-a      default-deny   3m55s   True

Adım 3 — privileged, hostNetwork ve label'sız pod'lara admission'da ret

Default-deny'ın delikleri pod spec'inde. hostNetwork: true olan pod node'un ağını kullanıyor; namespace policy'si ona uygulanmıyor. privileged: true olan container /sys/fs/bpf'e yazabiliyor, yani Tetragon'un programlarını kaldırabiliyor. app label'ı olmayan pod'u hiçbir CNP seçemiyor. Üçü de admission'da yakalanıyor:

# policies/disallow-privileged.yaml (özü)
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
  name: disallow-privileged
spec:
  validationActions: [Audit]
  matchConstraints:
    namespaceSelector:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: NotIn
          values: [kube-system, kube-public, kube-node-lease, kyverno, cilium-secrets, local-path-storage, tetragon]
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  evaluation:
    background:
      enabled: true
  autogen:
    podControllers:
      controllers: [deployments, statefulsets, daemonsets, jobs, cronjobs]
  variables:
    - name: allContainers
      expression: >-
        object.spec.containers + (has(object.spec.initContainers) ? object.spec.initContainers : [])
  validations:
    - expression: >-
        variables.allContainers.all(c,
          !has(c.securityContext) || !has(c.securityContext.privileged) || c.securityContext.privileged == false)
      message: "privileged containers are not allowed: they can disable the eBPF runtime layer (Tetragon)"
  • autogen.podControllers. Kural Pod için yazılıyor ama Deployment ve diğerlerine de uygulanıyor; hata ReplicaSet'in sessizce pod yaratamadığı yerde değil, Deployment apply anında çıkıyor. Lab'da latest tag'li bir Deployment'ı doğrudan Deployment seviyesinde reddetti.
  • evaluation.background: true. Kural sadece yeni isteklere değil, cluster'da zaten duran pod'lara da çalışıyor ve sonucu PolicyReport'a yazıyor. Audit aşamasının anlamı bu.
  • Hariç tutma listesi her policy'de aynı. Bu bir zevk meselesi değil; neden olduğu #5'te.

hostNetwork kuralı aynı iskelette tek ifade: !has(object.spec.hostNetwork) || object.spec.hostNetwork == false. app label kuralı da öyle: has(object.metadata.labels) && 'app' in object.metadata.labels.

Gateway kuralı farklı bir API grubuna bakıyor ve ilk günden Deny. Tenant HTTPRoute yazar, Gateway platformundur:

# policies/restrict-gateway-namespace.yaml (özü)
  validationActions: [Deny]
  matchConstraints:
    resourceRules:
      - apiGroups: ["gateway.networking.k8s.io"]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["gateways"]
  validations:
    - expression: "object.metadata.namespace == 'platform'"
Error from server: error when creating "demo/bad-gateway.yaml": admission webhook "vpol.validate.kyverno.svc-fail-86e66184" denied the request: Policy restrict-gateway-namespace failed: Gateway objects live in the platform namespace only (got team-a); use an HTTPRoute

Adım 4 — Kyverno MutatingPolicy: eksik label'ı reddetmek yerine eklemek

app label'ı zorunlu demek, eski Helm chart'ları olan ekiplere "chart'ını değiştir" demek. Deployment'ın adı zaten var; label'ı ondan türetiyoruz. Mutating admission, validating'den önce çalışıyor: label ekleniyor, validate eklenmiş halini görüyor.

# policies/mutate-name-label.yaml (özü)
apiVersion: policies.kyverno.io/v1
kind: MutatingPolicy
metadata:
  name: mutate-app-label-from-name
spec:
  matchConditions:
    - name: template-has-no-app-label
      expression: >-
        !(has(object.spec.template.metadata) && has(object.spec.template.metadata.labels) &&
          'app' in object.spec.template.metadata.labels)
  mutations:
    - patchType: ApplyConfiguration
      applyConfiguration:
        expression: |
          Object{
            spec: Object.spec{
              template: Object.spec.template{
                metadata: Object.spec.template.metadata{
                  labels: Object.spec.template.metadata.labels{
                    app: object.metadata.name
                  }
                }
              }
            }
          }
--- unlabelled Deployment, labels after mutation:
deployment.apps/echo created
{"app":"echo"}
{"app":"echo","role":"echo"}

role ekibin kendi label'ı, app Kyverno'nun eklediği. matchConditions sayesinde label'ı zaten olan Deployment'a dokunulmuyor.

Adım 5 — Kyverno kütüphanesinden 3 temel kural: latest tag, default namespace, requests/limits

Buraya kadarki kurallar bu zincire özel. Bir de hemen her cluster'da ilk istenen kurallar var. Kyverno'nun policy kütüphanesinin best-practices-vpol klasöründe 15 kural var; üçünü aldım, çünkü üçü de "çalışıyor ama bir gün patlayacak" türünden hatalar ve üçü de bir satırla düzeltiliyor. Hangisinin daha "popüler" olduğunu ölçen bir veri bulamadım; seçim bu üç gerekçeye dayanıyor.

Kural Kütüphanedeki yeri Neyi önlüyor
disallow-latest-tag best-practices-vpol/disallow-latest-tag latest hareketli: bu gece yeniden başlayan pod, test ettiğiniz image'ı değil başka bir image'ı çalıştırabilir
disallow-default-namespace best-practices-vpol/disallow-default-namespace -n verilmeyen her apply default'a düşüyor: sahibi, kotası, ekip RBAC'ı olmayan ortak alan
require-requests-limits best-practices-vpol/require-pod-requests-limits Request yoksa scheduler pod'ları körlemesine yerleştiriyor; bellek limiti yoksa tek bir sızıntı node'u düşürüyor

CEL ifadelerini kütüphaneden değiştirmeden aldım; sadece diğer kurallardaki sistem namespace hariç tutmasını ve autogen'i ekledim. latest kuralı:

# policies/disallow-latest-tag.yaml (validations)
  validations:
    - expression: "object.spec.containers.all(container, container.image.contains(':'))"
      message: "An image tag is required."
    - expression: "object.spec.containers.all(container, !container.image.endsWith(':latest'))"
      message: "Using a mutable image tag e.g. 'latest' is not allowed."

default namespace kuralı pod'un kendisine değil, bulunduğu namespace'e bakıyor (namespaceObject):

# policies/disallow-default-namespace.yaml (validations)
  validations:
    - expression: "namespaceObject.metadata.name != 'default'"
      message: "Using 'default' namespace is not allowed."

Kütüphane bu kuralda autogen'i kapalı tutuyor: Deployment namespace'ini template'te değil kendi metadata'sında taşıyor. Requests/limits kuralı containers, initContainers ve ephemeralContainers'ın hepsine bakıyor; dosyası klasörde.

Üç kural da Audit'te kuruldu. İlk PolicyReport:

default              disallow-default-namespace   Pod/quick-test                       Using 'default' namespace is not allowed.
local-path-storage   require-requests-limits      Pod/local-path-provisioner-75f7fc7dc5-twhg7 CPU and memory resource requests and memory limits are required for containers.
local-path-storage   require-requests-limits      Deployment/local-path-provisioner    CPU and memory resource requests and memory limits are required for containers.
team-a               disallow-latest-tag          Pod/careless                         Using a mutable image tag e.g. 'latest' is not allowed.
team-a               require-requests-limits      Pod/careless                         CPU and memory resource requests and memory limits are required for containers.
team-a               require-requests-limits      Pod/good                             CPU and memory resource requests and memory limits are required for containers.
team-a               require-requests-limits      Deployment/echo                      CPU and memory resource requests and memory limits are required for containers.
...

12 ihlal. quick-test, careless ve bad bilerek kötü yazılmış demo pod'ları. Ama good ve echo lab'ın "iyi örnek" diye yazdığı manifestlerdi ve ikisinde de resources yoktu. local-path-provisioner ise kind'ın kendi bileşeni; bu iki kural sistem namespace listesinde local-path-storage'ı unutmuştu. İlk günden Deny açsaydım ikisi de reddedilecekti. Audit tam olarak bunun için var: önce liste, sonra düzeltme, sonra Deny.

Düzeltme iki satır: iyi manifestlere resources, hariç tutma listesine local-path-storage. Sonraki rapor sadece üç demo pod'unu gösterdi.

Adım 6 — Audit'ten Deny'a ve CI'da kyverno apply

Deny'a geçiş policy dosyalarında yapılıyor, kubectl patch ile değil (#4'te neden):

# scripts/enforce.sh (özü)
for p in require-app-label disallow-host-network disallow-privileged \
         disallow-latest-tag require-requests-limits disallow-default-namespace; do
  f=$(grep -l "name: $p\$" policies/*.yaml)
  sed -i.bak 's/validationActions: \[Audit\]/validationActions: [Deny]/' "$f" && rm -f "$f.bak"
done
kubectl apply -f policies/

Aynı bad pod'u tekrar apply edince:

Error from server: error when creating "demo/bad-pod.yaml": admission webhook "vpol.validate.kyverno.svc-fail-01f0a879" denied the request: Policy disallow-host-network failed: hostNetwork is not allowed: it bypasses CiliumNetworkPolicy and the namespace default-deny;
  Policy disallow-privileged failed: privileged containers are not allowed: they can disable the eBPF runtime layer (Tetragon);
  Policy require-app-label failed: pod bad has no 'app' label; Cilium policies select on it;
  Policy require-requests-limits failed: CPU and memory resource requests and memory limits are required for containers.

Dört ihlal, tek mesajda. latest tag'li bir Deployment da Deployment seviyesinde reddedildi; good pod çalışmaya devam ediyor.

Son parça: aynı kuralları PR'da, cluster'sız çalıştırmak. kyverno apply policy'leri bir manifest dizinine uyguluyor:

# ci/kyverno-apply.sh (özü)
kyverno apply policies/ --resource "$MANIFESTS" --values-file ci/values.yaml --crd-paths "$CRDS"
policy restrict-gateway-namespace -> resource team-a/Gateway/rogue failed:
policy disallow-host-network -> resource team-a/Pod/bad failed:
policy disallow-privileged -> resource team-a/Pod/bad failed:
policy require-app-label -> resource team-a/Pod/bad failed:
policy require-requests-limits -> resource team-a/Pod/bad failed:
policy disallow-latest-tag -> resource team-a/Pod/careless failed:
policy require-requests-limits -> resource team-a/Pod/careless failed:
policy disallow-default-namespace -> resource default/Pod/quick-test failed:
pass: 25, fail: 8, warn: 0, error: 0, skip: 0

Cluster'daki 8 ihlalin hepsi, çıkış kodu sıfırdan farklı, PR kırmızı. --values-file ve --crd-paths olmadan bu sayı farklıydı; #7.

Zincir tamam: Kyverno kurala uymayan pod'u içeri almıyor, içeri girenler Cilium'un default-deny'ı altında. Bir sonraki katman, içeride çalışan process'lerin ne yaptığını izlemek (Tetragon gibi bir runtime aracı); o bu yazının kapsamı dışında.

Nerede patladı: 9 gerçek hata ve çözümü

1. resource gateways not found: Gateway CRD'leri Kyverno'dan sonra kurulunca

İlk make up CRD'leri hiç kurmuyordu; Gateway policy'si READY false kaldı. CRD'leri sonradan kurdum, bu kez Gateway isteği yanlış sebeple reddedildi:

admission webhook "vpol.validate.kyverno.svc-fail-86e66184" denied the request: resource gateways not found in group gateway.networking.k8s.io/v1

Kyverno kaynak türlerinin listesini başlarken alıyor. Kyverno controller'larını yeniden başlatınca düzeldi. make up artık CRD'leri Cilium'dan ve Kyverno'dan önce kuruyor.

2. Policy is not ready for reporting: reports controller'ın Gateway yetkisi yoktu

Restart'tan sonra bile status şuydu:

WebhookConfigured=True; RBACPermissionsGranted=False Policy is not ready for reporting, missing permissions: get gateway.networking.k8s.io/v1, Resource=gateways

Reddetme çalışıyor (webhook kurulu), ama reports controller Gateway'leri okuyamadığı için PolicyReport yazamıyor. Reports controller'a aggregated bir rolle get/list/watch gateways verince READY true oldu. CRD'ler baştan kurulu olsa da bu rol gerekiyor; kaldırıp denedim.

3. Kyverno kapalıyken: pod içeri girdi, yeni namespace default-deny almadı

İki ayrı kesinti denedim:

  • Admission controller'ı 0'a ölçekledim. Kyverno düzgün kapanırken kendi webhook'larını siliyor (chart'ta webhooksCleanup.enabled: true): yedi webhook yapılandırması ikiye düştü. Policy'ler Deny'dayken latest tag'li ve resources'suz careless pod'u kabul edildi. Kyverno geri gelince pod çalışmaya devam etti; background taraması onu PolicyReport'a 2 ihlalle yazdı, ama durdurmadı. Planlı bir kapatma, ölçekleme ya da yükseltme anında kapı açık.
  • Kyverno ayakta, servisi ulaşılamaz. Webhook'lar yerinde kaldı, pod failed calling webhook ... no route to host ile reddedildi (Fail). kube-system'de pod açılabildi (selector), yeni namespace de açıldı (generate webhook'u Ignore).

Her iki kesintide açılan namespace'ler (team-b, team-c) Kyverno geri geldikten 2 dakika sonra bile default-deny almamıştı. Yazının çözdüğü "unutulan namespace" sorunu Kyverno kesintisinde geri geliyor. generateExisting sadece policy oluşturulurken çalışan tek seferlik bir tarama; policy'ye annotation eklemek onu tetiklemedi. Tetikleyen şey spec'i değiştirmek oldu:

kubectl patch generatingpolicy generate-default-deny --type merge -p '{"spec":{"evaluation":{"generateExisting":{"enabled":false}}}}'
kubectl patch generatingpolicy generate-default-deny --type merge -p '{"spec":{"evaluation":{"generateExisting":{"enabled":true}}}}'

10 saniye içinde iki namespace de default-deny aldı. Kyverno kesintisinden sonraki runbook'a bu iki satır giriyor. Production'da admission controller'ı 3 replica ve PodDisruptionBudget ile çalıştırmak da kesinti penceresini daraltıyor.

4. kubectl apply Deny'ı sessizce Audit'e geri aldı

İlk enforce.sh policy'leri kubectl patch ile Deny'a çeviriyordu. Sonra hariç tutma listesini düzeltip kubectl apply -f policies/ dedim; dosyalarda Audit yazdığı için yedi policy'nin hepsi Audit'e döndü ve bad pod kabul edildi. Hata yok, uyarı yok. Bir Argo CD sync'i de aynısını yapardı. Deny'a geçiş artık dosyalarda; gerçek bir repoda bu bir PR.

5. Dört ihlal, hata mesajında bir tane: her namespaceSelector için ayrı webhook

İlk Deny denemesinde bad pod'un raporda 4 ihlali vardı, ama hata mesajı sadece require-app-label'ı söylüyordu. Kyverno policy'leri namespaceSelector'larına göre gruplayıp her farklı hariç tutma listesi için ayrı bir webhook açıyor; beş pod kuralımda üç farklı liste vardı, listesi olmayan iki kural (default namespace ve Gateway) da dördüncü grubu oluşturuyordu: dört vpol webhook'u. API server ilk reddeden webhook'un cevabını döndürüyor. Listeleri tek bir listede birleştirince webhook sayısı ikiye indi ve dört ihlalin hepsi tek mesajda geldi. Geliştirici için fark büyük: dört kez apply yerine bir kez.

6. synchronize: true elle düzenlenen CNP'yi geri yazmıyor

synchronize: true'nun elle yapılan değişikliği geri alacağını düşünüyordum. team-a'daki default-deny'ın açıklamasını kubectl patch ile değiştirdim; 45 saniye sonra değişiklik hâlâ yerindeydi. CNP'yi sildiğimde ise 5 saniyede geri geldi. Yani biri default-deny'ı düzenleyip gevşetirse Kyverno bunu fark etmiyor. Bunu kapatmanın yolu, network.platform/managed-by=kyverno label'lı CNP'lerde UPDATE'i reddeden bir ValidatingPolicy; lab'da denemedim.

7. CI'da error: 5: namespaceObject boş, Gateway hiç değerlendirilmedi

İlk kyverno apply 6 fail ve 5 error verdi. Hataların beşi de disallow-default-namespace'ten geliyordu: cluster'sız çalışırken namespaceObject boş. Çözüm, Kyverno'nun kendi CLI testlerindeki gibi namespace'leri bir values dosyasında vermek (ci/values.yaml, namespaces: listesi). Bir de sessiz bir eksik vardı: Gateway kuralı CI'da hiç çalışmıyordu, sonuç 0 idi, hata yoktu. CLI Gateway türünü tanımıyordu; --crd-paths ile Gateway CRD'sini verince rogue Gateway de fail oldu. --crd-paths adına rağmen dizin kabul etmiyor: CRD file must have .yaml or .yml extension.

8. Hariç tutulan namespace PolicyReport'ta kaldı

local-path-storage'ı hariç tutma listesine ekledikten sonra eski ihlal satırları PolicyReport'ta kalmaya devam etti. Raporları silince temizlendi ve geri gelmedi. Hariç tutma listesini değiştirdiğinizde eski rapor satırlarına güvenmeyin.

9. permission denied ... ciliumnetworkpolicies: Kyverno generate'i tekrar denemiyor

Background controller'ın cilium.io rolünü kaldırıp yeni bir namespace açtım. Namespace açıldı, CNP gelmedi; hata sadece background controller'ın log'undaydı:

failed to generate resources: permission denied: ciliumnetworkpolicies.cilium.io "default-deny" is forbidden: User "system:serviceaccount:kyverno:kyverno-background-controller" cannot get resource "ciliumnetworkpolicies" in API group "cilium.io" in the namespace "team-d"

Rolü geri yükledim; 2 dakika boyunca hiçbir şey olmadı, Kyverno başarısız generate'i tekrar denemiyor. #3'teki generateExisting toggle'ı burada da çalıştı. Kurulumda sıra bu yüzden önemli: önce rbac/, sonra policies/.

Doğrulama

make verify
$ kubectl -n team-a get cnp
NAME           AGE    VALID
default-deny   6m2s   True

$ kubectl get validatingpolicies -o custom-columns='NAME:.metadata.name,ACTIONS:.spec.validationActions'
NAME                         ACTIONS
disallow-default-namespace   [Deny]
disallow-host-network        [Deny]
disallow-latest-tag          [Deny]
disallow-privileged          [Deny]
require-app-label            [Deny]
require-requests-limits      [Deny]
restrict-gateway-namespace   [Deny]

$ kubectl apply -f demo/bad-pod.yaml
Error from server: error when creating "demo/bad-pod.yaml": admission webhook "vpol.validate.kyverno.svc-fail-01f0a879" denied the request: Policy disallow-host-network failed: ...; Policy disallow-privileged failed: ...; Policy require-app-label failed: ...; Policy require-requests-limits failed: ...
  • Yeni bir namespace açın ve kubectl -n <ns> get cnp deyin: default-deny birkaç saniye içinde orada olmalı.
  • kubectl get validatingpolicies -o custom-columns=NAME:.metadata.name,READY:.status.conditionStatus.ready: yedisi de true.
  • make ci: pass: 25, fail: 8, warn: 0, error: 0. error 0 değilse values ya da CRD dosyası eksik.
  • make audit dosyaları başlangıç durumuna döndürüyor, make down cluster'ı siliyor.

UI'da: make reporter Kyverno'nun PolicyReport'larını gösteren Policy Reporter'ı kuruyor. Onun namespace'i de Kyverno'dan default-deny alıyor; Policy Reporter API server'a ulaşabilsin diye yanına ikinci bir CNP koydum, üretilen default-deny'a dokunmadan (Cilium iki policy'yi birleştiriyor). make ui-reports → http://localhost:8082: demo pod'ları Audit'teyken Dashboard 71 pass, 7 fail gösterdi. make ui-hubble → http://localhost:12000, namespace team-a: demo/curl-pod.yaml'dan https://example.com'a giden istekler dropped, DNS çalışıyor.

Kaynaklar: Kubernetes dokümanları Dynamic Admission Control, Admission Control, Validating Admission Policy, Mutating Admission Policy; Kyverno policy kütüphanesi, Kyverno 1.19.1 Helm notları ve migration to CEL rehberi, security vs operability bölümü; Kyverno CLI testleri (test/cli/test-validating-policy/with-namespaceObject-2).

Önceki yazı

Secret'ları Kasaya Taşımak: Git'teki base64'ten Vault ve ESO'ya Kesintisiz Geçiş

Bu yazının kodu: https://github.com/miraccan00/blog-wiki/tree/main/kyverno-cilium-policy-chain

İnsanın en büyük hatalarından biri de doğru zamanı yanlış kişilerle doldurmaktır.
Charles Bukowski