Skip to main content
BlogSecurity

Kubernetes Manifest-Based Admission Control Beta di 1.37

Kubernetes 1.37 membawa manifest-based admission control ke Beta: policy dan webhook dimuat dari disk, tahan etcd mati, dan bisa melindungi dirinya sendiri.

Matthews Wong

Software Engineer

Oktober 202612 menit baca
Kubernetes Manifest-Based Admission Control Beta di 1.37

Ringkasan Utama

Manifest-based admission control, Beta dan aktif secara default di Kubernetes 1.37, memuat admission webhook dan CEL policy dari file di staticManifestsDir. Policy ditegakkan sejak API server start, tetap jalan saat etcd mati, dan bisa mencegah penghapusan resource admission berbasis API. Harganya: file per server, webhook hanya URL, dan tanpa parameter.

Setiap admission policy yang pernah saya deploy ke cluster punya satu kelemahan yang sama: ia adalah API object, jadi baru ada setelah API server hidup dan etcd menjawab, dan bisa dihapus siapa pun yang RBAC-nya mengizinkan. Saat restore dari backup, di situlah policy yang kita andalkan belum ada sama sekali.

Kubernetes 1.37 (Garhwal, rilis 26 Agustus 2026) menaikkan manifest-based admission control ke Beta dan mengaktifkannya secara default. Tulisan ini adalah pembacaan dekat atas halaman docs resmi, pengumuman alpha v1.36, release post 1.37, dan KEP-5793. Saya belum menjalankannya di control plane production, jadi semua di bawah ini adalah isi dokumen tersebut, ditambah konfigurasi yang saya susun dari contoh mereka dan saya tandai bila demikian.

Tiga celah yang tidak bisa ditutup admission berbasis API

Docs menyebut tiga batasan policy yang didaftarkan lewat API. Bootstrap gap: API object harus dibuat dan dimuat oleh dynamic admission controller, dan sebelum itu terjadi tidak ada yang ditegakkan. Self-protection gap: resource konfigurasi admission sendiri tidak dikenai webhook admission demi menghindari circular dependency, sehingga user dengan privilege cukup bisa menghapus atau mengubah policy penting. Ketergantungan etcd: bila etcd tidak tersedia atau rusak, policy berbasis API bisa gagal dimuat.

Pengumuman v1.36 menjelaskan celah pertama dalam bahasa operasional: saat restore dari backup atau pulih dari kegagalan etcd, jeda antara API server mulai melayani request dan policy menjadi aktif bisa cukup lama. KEP-5793 mencantumkan persis ini sebagai user story: cluster hasil restore, etcd yang di-reset, atau bootstrap baru, di mana security webhook harus terpanggil sejak request pertama.

KEP juga menjelaskan mengapa workaround biasa kurang. Configuration management eksternal seperti Helm, Kustomize atau GitOps tidak menutup bootstrap gap dan konfigurasinya tetap bisa dihapus lewat API. RBAC deny rule butuh redesign besar. Meng-compile admission plugin custom ke binary API server butuh build khusus. File yang dibaca langsung oleh API server melewati ketiganya.

Cara staticManifestsDir menyambungkannya

Mekanismenya satu field. Anda menambahkan staticManifestsDir ke konfigurasi plugin di file AdmissionConfiguration, lalu memberikan file itu ke kube-apiserver lewat --admission-control-config-file. Feature gate ManifestBasedAdmissionControlConfig harus aktif; menurut docs, gate ini aktif default sejak 1.37, sedangkan di 1.36 masih alpha dan mati default. Masing-masing dari empat plugin, ValidatingAdmissionWebhook, MutatingAdmissionWebhook, ValidatingAdmissionPolicy dan MutatingAdmissionPolicy, butuh direktorinya sendiri.

Konfigurasi di bawah adalah contoh docs yang saya ringkas menjadi dua plugin yang akan saya mulai. Fragmen kube-apiserver sesudahnya adalah ilustrasi saya sendiri untuk static pod gaya kubeadm, bukan resep dari docs; post v1.36 hanya menyebut bahwa bila API server berjalan sebagai Pod, direktorinya harus di-mount, dan read-only sudah cukup.

# /etc/kubernetes/admission/admission-config.yaml
# Passed to kube-apiserver with --admission-control-config-file.
# Absolute paths only: no globs, no relative paths.
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ValidatingAdmissionPolicy
  configuration:
    apiVersion: apiserver.config.k8s.io/v1
    kind: ValidatingAdmissionPolicyConfiguration
    staticManifestsDir: "/etc/kubernetes/admission/validating-policies/"
- name: ValidatingAdmissionWebhook
  configuration:
    apiVersion: apiserver.config.k8s.io/v1
    kind: WebhookAdmissionConfiguration
    kubeConfigFile: "<path-to-kubeconfig>"
    staticManifestsDir: "/etc/kubernetes/admission/validating-webhooks/"

# My illustration, not from the docs: a kubeadm-style static pod fragment.
# The directory must be visible INSIDE the kube-apiserver container;
# read-only is enough because the API server only reads it.
#   command:
#   - kube-apiserver
#   - --admission-control-config-file=/etc/kubernetes/admission/admission-config.yaml
#   volumeMounts:
#   - name: admission
#     mountPath: /etc/kubernetes/admission
#     readOnly: true
#   volumes:
#   - name: admission
#     hostPath:
#       path: /etc/kubernetes/admission
#       type: Directory

Tiga detail menentukan apakah ini langsung berhasil. Path harus absolut; glob dan path relatif tidak didukung. Hanya file langsung di direktori dengan ekstensi .yaml, .yml atau .json yang dimuat, subdirektori dan ekstensi lain diabaikan. Dan setiap direktori hanya boleh berisi tipe resource yang diterima plugin-nya, jadi direktori ValidatingAdmissionPolicy hanya boleh berisi policy dan binding.

Apa yang berubah dibanding policy berbasis API

Pertukarannya adalah isolasi dengan fleksibilitas. Konfigurasi manifest-based hidup dalam apa yang KEP-5793 sebut isolated universe: ia tidak boleh mereferensikan API resource, dan sebaliknya. Satu aturan itu menjelaskan setiap batasan di tabel berikut, karena policy yang butuh ConfigMap atau Service bergantung pada state cluster yang justru mungkin belum ada saat startup.

AspekAdmission berbasis APIAdmission berbasis manifest
LokasinyaObject yang disimpan lewat Kubernetes APIFile di staticManifestsDir, tidak terlihat atau bisa diubah lewat API
Aktif sejakSetelah object dibuat dan dimuat dynamic admission controllerBegitu API server siap melayani request
Saat etcd matiPolicy bisa gagal dimuatTidak bergantung pada ketersediaan etcd
Bisa mencegat resource admissionTidak, dilewati untuk menghindari circular dependencyYa, dan hanya hook manifest-based yang berjalan untuk tipe itu
Backend webhookService atau URLHanya clientConfig.url; field service tidak diizinkan
Parameter policyparamKind dan paramRefKeduanya ditolak; binding harus menunjuk policy di set manifest yang sama
Nama objectNama valid apa sajaHarus berakhiran .static.k8s.io
Urutan evaluasiSetelah konfigurasi manifest-basedSebelum konfigurasi berbasis API
Cara updateUbah API objectEdit file; dipantau dengan fsnotify dan fallback polling, default 1 menit
Beberapa API serverSatu object, dibagi lewat etcdTiap server memuat file sendiri; tidak ada sinkronisasi antar server

File manifest diproses strict decoder yang menolak field duplikat dan field tak dikenal, dan setiap object mendapat defaulting serta validasi yang sama seperti REST API. Policy dan binding-nya bisa dikelompokkan dengan v1.List atau pemisah ---, dan docs menyebut policy beserta binding-nya dimuat bersamaan secara atomik.

Melindungi si pelindung

Kemampuan utamanya adalah yang tidak bisa ditawarkan admission berbasis API: policy yang mencegat operasi pada ValidatingAdmissionPolicy, MutatingAdmissionPolicy, binding-nya, dan dua tipe konfigurasi webhook. Contoh di docs menolak DELETE dan UPDATE pada resource mana pun yang berlabel platform.example.com/protected bernilai true. Ekspresinya meloloskan request kecuali object lama punya label itu, makanya yang dicek adalah oldObject, bukan object.

Simpan sebagai file di direktori ValidatingAdmissionPolicy. Nama object berakhiran .static.k8s.io, failurePolicy bernilai Fail, dan binding memakai validation action Deny. Ini contoh dari docs, dengan nama aslinya.

# /etc/kubernetes/admission/validating-policies/protect-admission-resources.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "example-protect-admission-resources.static.k8s.io"   # suffix is mandatory
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups: ["admissionregistration.k8s.io"]
      apiVersions: ["*"]
      operations: ["DELETE", "UPDATE"]
      resources:
      - "validatingadmissionpolicies"
      - "validatingadmissionpolicybindings"
      - "mutatingadmissionpolicies"
      - "mutatingadmissionpolicybindings"
      - "validatingwebhookconfigurations"
      - "mutatingwebhookconfigurations"
  validations:
  # oldObject, not object: on DELETE there is no new object to inspect.
  - expression: >-
      !has(oldObject.metadata.labels) ||
      !('platform.example.com/protected' in oldObject.metadata.labels) ||
      oldObject.metadata.labels['platform.example.com/protected'] != 'true'
    message: "Protected admission resources cannot be modified or deleted"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "example-protect-admission-resources-binding.static.k8s.io"
spec:
  # Must name a policy in the same manifest set, with the same suffix.
  # spec.paramRef is not allowed here.
  policyName: "example-protect-admission-resources.static.k8s.io"
  validationActions:
  - Deny

Urutan juga penting. Konfigurasi manifest-based dievaluasi sebelum yang berbasis API, jadi policy platform menang atas apa pun yang didaftarkan tim belakangan. Untuk tipe resource admission itu sendiri, hanya hook manifest-based yang berjalan; hook berbasis API dilewati. TokenReview dan SubjectAccessReview dikecualikan untuk semua, karena mencegatnya bisa mengunci cluster dari jalur authentication dan authorisation-nya sendiri.

Policy ini adalah gembok yang kuncinya ada di node. Berdasarkan ekspresinya sendiri, ia juga menolak GitOps controller yang mencoba meng-update object terlindungi, jadi memberi label protected berarti setiap perubahan ke depan harus lewat file di disk. Itu memang tujuannya, dan itu pula hal pertama yang akan mengejutkan tim pemilik resource berlabel tersebut.

Semantik reload: ketat saat boot, toleran saat runtime

Saat startup dan runtime aturannya berlawanan, dan itu disengaja. Saat startup setiap manifest dibaca dan divalidasi, API server tidak menjadi ready sampai semuanya berhasil dimuat, dan manifest yang tidak valid membuatnya gagal start. Post v1.36 membela ini: gagal cepat lebih aman daripada berjalan tanpa policy yang diharapkan. Hal yang sama berlaku bila dua file mendefinisikan tipe dan nama yang sama.

Saat runtime, direktori dipantau dengan fsnotify dan fallback polling, default satu menit. Hash konten semua file manifest dihitung di setiap pengecekan, dan tidak ada yang terjadi bila hash tidak berubah. Konfigurasi baru divalidasi dulu; bila gagal, error di-log, metrics diperbarui, dan konfigurasi valid sebelumnya tetap dipakai. Reload yang sukses mengganti konfigurasi lama secara atomik.

Docs meminta perubahan ditulis secara atomik, misalnya ke file sementara lalu di-rename, supaya watcher tidak pernah membaca file yang setengah tertulis. Ini paling penting untuk ConfigMap dan Secret yang di-mount. Karena file tanpa ekstensi .yaml, .yml atau .json diabaikan, nama staging dengan ekstensi lain tidak terlihat oleh loader sampai di-rename.

# Wrong: edit in place. The watcher can read a half-written file,
# fail validation, and keep the OLD policy while you think the new one is live.
vi /etc/kubernetes/admission/validating-policies/deny-privileged.yaml

# Right: stage under a name the loader ignores (only .yaml, .yml and .json
# are loaded), then rename. A rename within one filesystem is atomic.
D=/etc/kubernetes/admission/validating-policies
cp deny-privileged.yaml "$D/deny-privileged.yaml.staging"
mv "$D/deny-privileged.yaml.staging" "$D/deny-privileged.yaml"

# Then confirm the reload actually succeeded. Do not trust the mv.
# Metric names are from the docs; the 1 minute poll is the default fallback.
curl -sk --cert "$CERT" --key "$KEY" https://127.0.0.1:6443/metrics \
  | grep apiserver_manifest_admission_config_controller

Reload yang ditolak tidak terasa bagi orang yang mendorong perubahan: policy lama tetap berjalan dan hanya satu baris log serta sebuah metric yang memberi tahu. Perlakukan counter kegagalan sebagai hasil test setiap rollout policy, bukan alert yang dibaca belakangan.

Di mana ia menggigit: trade-off yang dijabarkan docs

KEP-5793 mencantumkan kekurangannya sendiri tanpa dihaluskan: visibilitas berkurang karena Anda tidak bisa mendaftar admission control aktif lewat API; kompleksitas operasional di cluster HA; debugging lebih sulit karena penolakan bisa berasal dari file yang tidak bisa di-kubectl get; dan fungsionalitas terbatas. Tabel troubleshooting di docs mengubahnya menjadi gejala. Dua hal lagi yang saya masukkan ke checklist rollout berasal dari bagian lain halaman: downgrade dengan staticManifestsDir masih terkonfigurasi membuat API server gagal start karena field tak dikenal, dan di 1.37 webhook yang mencegat request review authentication atau authorisation diam-diam berhenti menerimanya.

GejalaKemungkinan penyebabPenyelesaian
API server tidak mau startManifest tidak valid saat startupBaca log API server untuk error validasi, perbaiki file, restart
API server tidak mau startNama object duplikat antar file manifestBuat nama unik di setiap direktori plugin
Policy tidak berlaku setelah file dieditValidasi reload gagal; konfigurasi sebelumnya dipertahankanCek metric kegagalan reload dan log, perbaiki file, tunggu siklus berikutnya
Panggilan webhook gagalclientConfig.url tidak terjangkau dari kube-apiserverVerifikasi keterjangkauan dari host API server; KEP menyarankan failurePolicy Ignore hanya untuk mengisolasi masalah
Tidak bisa membuat API objectNama berakhiran .static.k8s.io, suffix yang dicadangkan selama gate aktifGanti nama API object
Keputusan admission berbeda antar API serverConfiguration drift di control plane HASinkronkan file dengan configuration management; bandingkan label hash pada metric config info
Webhook berhenti menerima SubjectAccessReview atau TokenReview setelah upgradeExcludeAdmissionWebhookVirtualResources, aktif default di 1.37Hapus rule tersebut; KEP menawarkan gate bernilai false sebagai jalan keluar sementara sebelum dikunci saat GA

Baris terakhir pantas dapat kalimat sendiri karena sama sekali bukan soal manifest. KEP-5793 menyertakan perubahan perilaku dalam beta: ValidatingAdmissionWebhook dan MutatingAdmissionWebhook kini melewati virtual resource authentication dan authorisation yang sudah dilewati CEL policy. Ini opt-out jangka pendek, akan dikunci saat GA, dan KEP menyebut ada deprecation warning saat menulis rule webhook yang menyebut resource tersebut. Audit rule webhook Anda sebelum upgrade, bukan sesudahnya.

Observability: Anda tidak bisa kubectl get

Karena tidak ada yang terlihat lewat API, docs bersandar pada tiga sinyal. Metrics: counter reload dengan label status, plugin dan apiserver_id_hash, gauge timestamp reload terakhir, dan gauge config info yang label hash-nya menunjukkan drift antar API server. Metric admission yang sudah ada seperti apiserver_admission_webhook_rejection_count membawa nama object, jadi suffix .static.k8s.io mengidentifikasi keputusan manifest-based. Audit annotation juga memuat nama object. Rule di bawah adalah PromQL buatan saya berdasarkan nama metric tersebut, untuk disesuaikan, bukan disalin.

# Alert 1: a reload was rejected, so the previous policy is still running.
- alert: ManifestAdmissionReloadFailed
  expr: increase(apiserver_manifest_admission_config_controller_automatic_reloads_total{status="failure"}[10m]) > 0
  labels:
    severity: page

# Alert 2: control plane nodes disagree about which files are loaded.
# last_config_info is always 1 and carries the hash as a label.
- alert: ManifestAdmissionConfigDrift
  expr: count by (plugin) (count by (plugin, hash) (apiserver_manifest_admission_config_controller_last_config_info)) > 1
  for: 15m
  labels:
    severity: ticket

# Who said no? Manifest-based decisions carry the .static.k8s.io suffix.
# sum by (name) (rate(apiserver_admission_webhook_rejection_count{name=~".*\.static\.k8s\.io"}[5m]))

Rule kedua bekerja karena nilai config info selalu 1 dan membawa hash sebagai label: bila jumlah hash berbeda untuk satu plugin lebih dari satu, setidaknya dua API server memegang file yang berbeda. Jalankan dengan klausa for yang cukup panjang untuk melewati push file bergulir, karena tiap server me-reload sesuai jadwalnya sendiri.

Yang dikerjakan hari Senin

Mulai dengan satu policy yang akan memalukan bila hilang, dan perlakukan sebagai perubahan control plane, bukan perubahan cluster:

  1. Pastikan Anda mengontrol flag kube-apiserver dan filesystem-nya. Saya tidak memverifikasi apakah ada provider managed Kubernetes yang membuka --admission-control-config-file, jadi pastikan dulu sebelum merencanakan.
  2. Latih di cluster 1.37 sekali pakai: file rusak harus menghentikan API server start, dan edit rusak harus membiarkan policy lama tetap jalan dengan counter kegagalan bertambah.
  3. Mulai dengan policy deny-privileged dari docs, lalu tambahkan policy protect-admission-resources setelah Anda memutuskan label mana yang menandai object terlindungi.
  4. Distribusikan file ke setiap node control plane dengan configuration management, dan pasang alert untuk counter kegagalan reload serta untuk lebih dari satu hash konfigurasi per plugin.
  5. Tulis catatan downgrade sekarang: hapus staticManifestsDir dari AdmissionConfiguration dan restart sebelum memindahkan control plane ke versi tanpa fitur ini.

Pengecekan berikut membuktikan kedua sisi fitur di cluster uji. Yang pertama menerapkan pod privileged di luar kube-system, yang seharusnya ditolak policy deny-privileged dengan pesannya sendiri. Yang kedua mencoba menghapus admission policy berlabel protected, yang seharusnya ditolak policy protect. Keduanya tidak memerlukan policy ada di etcd, dan itulah intinya.

# 1. Privileged pod outside kube-system: the deny-privileged policy should refuse it.
kubectl create namespace policy-test
cat <<'EOF' | kubectl apply -n policy-test -f -
apiVersion: v1
kind: Pod
metadata: { name: priv }
spec:
  containers:
  - name: c
    image: busybox:1.36
    command: ["sleep", "3600"]
    securityContext: { privileged: true }
EOF
# expect: Privileged containers are not allowed

# 2. Delete a policy labelled protected: the protect policy should refuse it.
kubectl label validatingadmissionpolicy my-baseline platform.example.com/protected=true
kubectl delete validatingadmissionpolicy my-baseline
# expect: Protected admission resources cannot be modified or deleted
# (the label UPDATE itself passes: oldObject had no label yet)

Aturan yang saya ambil: policy yang harus selamat dari kesalahan sebaiknya berada di luar sistem yang diaturnya. Manifest-based admission control memungkinkan itu untuk admission sendiri, dengan konsekuensi policy diperlakukan sebagai konfigurasi control plane yang harus didistribusikan, dipantau, dan di-downgrade secara manual. Pakai untuk segelintir rule yang ketiadaannya menyakitkan, dan biarkan yang berparameter atau dimiliki tim tetap di API.

FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Apa itu manifest-based admission control di Kubernetes?

Fitur yang membuat kube-apiserver memuat admission webhook dan CEL policy dari file di disk, diatur lewat staticManifestsDir di AdmissionConfiguration. Policy aktif sejak API server start, tidak bergantung pada etcd, dan tidak terlihat atau bisa diubah lewat Kubernetes API. Fitur ini mencapai Beta di Kubernetes 1.37.

Apakah manifest-based admission control aktif default di Kubernetes 1.37?

Feature gate ManifestBasedAdmissionControlConfig aktif default sejak 1.37, tetapi tidak ada yang berubah sampai Anda menambahkan staticManifestsDir ke file AdmissionConfiguration dan memberikannya lewat --admission-control-config-file. Menurut docs, cluster yang belum memakai konfigurasi manifest tidak mengalami perubahan perilaku.

Apakah admission policy dari disk tetap jalan saat etcd mati?

Ya. KEP menyatakan policy manifest-based tetap berfungsi saat etcd tidak tersedia, dan itu salah satu use case pendorongnya. Docs menggambarkannya independen dari etcd dan aktif begitu API server siap melayani request.

Bisakah ValidatingAdmissionPolicy dari file memakai parameter atau webhook Service?

Tidak. Policy manifest-based tidak bisa memakai paramKind, binding tidak bisa memakai paramRef, dan webhook harus memakai clientConfig.url, bukan referensi service. Konfigurasinya diisolasi dari API resource karena state cluster mungkin belum ada saat startup.

Apa yang terjadi jika file manifest tidak valid?

Saat startup, manifest tidak valid atau dua file dengan tipe dan nama sama membuat API server gagal start. Saat runtime, reload yang gagal dicatat di log dan metric sementara konfigurasi valid sebelumnya tetap aktif, jadi satu edit yang salah tidak menghapus policy Anda.