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: DirectoryTiga 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.
| Aspek | Admission berbasis API | Admission berbasis manifest |
|---|---|---|
| Lokasinya | Object yang disimpan lewat Kubernetes API | File di staticManifestsDir, tidak terlihat atau bisa diubah lewat API |
| Aktif sejak | Setelah object dibuat dan dimuat dynamic admission controller | Begitu API server siap melayani request |
| Saat etcd mati | Policy bisa gagal dimuat | Tidak bergantung pada ketersediaan etcd |
| Bisa mencegat resource admission | Tidak, dilewati untuk menghindari circular dependency | Ya, dan hanya hook manifest-based yang berjalan untuk tipe itu |
| Backend webhook | Service atau URL | Hanya clientConfig.url; field service tidak diizinkan |
| Parameter policy | paramKind dan paramRef | Keduanya ditolak; binding harus menunjuk policy di set manifest yang sama |
| Nama object | Nama valid apa saja | Harus berakhiran .static.k8s.io |
| Urutan evaluasi | Setelah konfigurasi manifest-based | Sebelum konfigurasi berbasis API |
| Cara update | Ubah API object | Edit file; dipantau dengan fsnotify dan fallback polling, default 1 menit |
| Beberapa API server | Satu object, dibagi lewat etcd | Tiap 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:
- DenyUrutan 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_controllerReload 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.
| Gejala | Kemungkinan penyebab | Penyelesaian |
|---|---|---|
| API server tidak mau start | Manifest tidak valid saat startup | Baca log API server untuk error validasi, perbaiki file, restart |
| API server tidak mau start | Nama object duplikat antar file manifest | Buat nama unik di setiap direktori plugin |
| Policy tidak berlaku setelah file diedit | Validasi reload gagal; konfigurasi sebelumnya dipertahankan | Cek metric kegagalan reload dan log, perbaiki file, tunggu siklus berikutnya |
| Panggilan webhook gagal | clientConfig.url tidak terjangkau dari kube-apiserver | Verifikasi keterjangkauan dari host API server; KEP menyarankan failurePolicy Ignore hanya untuk mengisolasi masalah |
| Tidak bisa membuat API object | Nama berakhiran .static.k8s.io, suffix yang dicadangkan selama gate aktif | Ganti nama API object |
| Keputusan admission berbeda antar API server | Configuration drift di control plane HA | Sinkronkan file dengan configuration management; bandingkan label hash pada metric config info |
| Webhook berhenti menerima SubjectAccessReview atau TokenReview setelah upgrade | ExcludeAdmissionWebhookVirtualResources, aktif default di 1.37 | Hapus 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:
- 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.
- 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.
- Mulai dengan policy deny-privileged dari docs, lalu tambahkan policy protect-admission-resources setelah Anda memutuskan label mana yang menandai object terlindungi.
- 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.
- 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.
Sumber dan bacaan lanjutan
- Docs Kubernetes: Manifest-Based Admission Control
- Blog Kubernetes: v1.36, Admission Policies That Can't Be Deleted
- Blog Kubernetes: rilis Kubernetes v1.37 (Garhwal)
- KEP-5793: Manifest Based Admission Control Config
- Docs Kubernetes: ValidatingAdmissionPolicy
- Docs Kubernetes: Dynamic Admission Control (webhook)

