Ringkasan Utama
Kubernetes 1.36 membawa empat perubahan keamanan: user namespaces dan fine-grained kubelet authorization masuk GA, volume gitRepo dinonaktifkan permanen, dan externalIPs pada Service dideprekasi dengan rencana penghapusan di v1.43. Hanya gitRepo yang langsung merusak manifest lama. Sisanya opt-in atau punya jadwal, jadi audit grant nodes/proxy, volume gitRepo, dan Service externalIPs sebelum upgrade.
Release post Kubernetes 1.36 mencantumkan 70 enhancement, dan pekerjaan keamanannya tersebar seperti uang receh di saku jaket. User namespaces ada di satu post, kubelet authorization di post lain, satu tipe volume dihapus diam-diam di post ketiga, dan field Service lama dideprekasi di post keempat. Saya ingin satu halaman yang menjelaskan mana dari empat perubahan ini yang bisa merusak manifest saat hari upgrade, dan mana yang hanya peluang.
Tulisan ini adalah pembacaan sumber primer: release post resmi 1.36, sneak peek, tiga post fitur dari blog Kubernetes, dan halaman docs terbaru untuk user namespaces, kubelet authorization, dan volumes. Saya tidak menjalankan satu pun dari ini di cluster untuk tulisan ini, jadi setiap angka dan perilaku di bawah dikaitkan ke halaman asalnya, dan script-nya adalah bahan untuk Anda sesuaikan, bukan benchmark. Cakupannya sengaja lebih sempit dari ulasan rilis 1.37 yang sudah ada di situs ini: hanya apa yang dilakukan 1.36 terhadap attack surface Anda.
Empat perubahan, satu tema: berhenti memercayai default
Pengumuman rilis 1.36 menghitung 70 enhancement: 18 naik ke Stable, 25 masuk Beta, dan 25 di Alpha, serta mengarahkan pembaca ke deprecation dan removal secara terpisah. Empat item keamanan mengikuti satu pola. Masing-masing mengambil sesuatu yang dulu serba longgar secara default, entah root di dalam container, permission kubelet yang tampak read-only, tipe volume yang menjalankan git sebagai root di node, atau field Service yang membiarkan user mana pun mengklaim IP mana pun, lalu menggantinya dengan yang lebih sempit.
Tabel ini adalah seluruh tulisan dalam satu layar. Kolom terpenting adalah kolom ketiga: hanya satu dari empat perubahan yang mengubah workload yang sudah ada pada hari Anda upgrade.
| Perubahan | Status di 1.36 | Dampak pada cluster yang ada | Tindakan Anda |
|---|---|---|---|
| User namespaces | Stable. Gate UserNamespacesSupport terkunci. Opt-in per pod dengan hostUsers bernilai false | Tidak ada sampai ada pod yang opt-in | Coba di satu node pool yang memenuhi minimum kernel dan runtime |
| Fine-grained kubelet authorization | Stable. Gate KubeletFineGrainedAuthz terkunci aktif | Tidak ada. Grant nodes/proxy yang ada tetap jalan lewat fallback | Pindahkan RBAC monitoring ke subresource sempit, lalu hapus nodes/proxy |
| Volume gitRepo | Dinonaktifkan permanen, tidak bisa dihidupkan lagi | Pod yang mendeklarasikan volume gitRepo rusak | Cari sebelum upgrade dan pindahkan proses clone ke init container |
| Service externalIPs | Deprecated. Warning sekarang, penghapusan direncanakan di v1.43 | Masih jalan, tetapi mengeluarkan warning | Inventarisasi, rencanakan pengganti, blokir pemakaian baru dengan admission controller |
Fine-grained kubelet authorization: get pada nodes/proxy tidak pernah read-only
Perubahan paling berguna bagi kebanyakan platform team justru yang paling tidak glamor. Kubelet membuka API HTTPS di node berisi daftar pod, metrics, log, dan kemampuan menjalankan perintah di dalam container. Dengan webhook authorization aktif, hampir semua path dulu dipetakan ke satu subresource nodes/proxy, sehingga agen monitoring yang hanya butuh metrics memerlukan permission yang sama dengan yang bisa exec ke container mana pun di node.
Halaman kubelet authentication dan authorization kini menyatakannya terang-terangan: sebagian endpoint memakai protokol Websocket lewat HTTP GET yang diotorisasi dengan verb get, sehingga get pada nodes/proxy bukan permission read-only dan mengizinkan eksekusi perintah di container mana pun di node. Post blog Kubernetes tentang GA menambahkan bahwa hal ini didemonstrasikan pada awal 2026 dan polanya muncul di RBAC default puluhan Helm chart yang banyak dipakai. Fiturnya masuk sebagai alpha di v1.32, beta aktif default di v1.33, dan GA di v1.36 lewat KEP-2862.
Cara kerjanya: untuk path yang terdaftar, kubelet lebih dulu mengirim SubjectAccessReview untuk subresource spesifik, dan hanya jika gagal ia mencoba ulang dengan nodes/proxy yang kasar demi backward compatibility. Fallback itulah sebabnya tidak ada yang rusak saat upgrade, dan juga sebabnya tidak ada yang lebih aman sampai grant lama dihapus.
| Path kubelet | Subresource yang dicek lebih dulu | Artinya untuk RBAC |
|---|---|---|
/stats/* | nodes/stats | Membaca stats tanpa kemampuan exec |
/metrics/* | nodes/metrics | Grant yang sebenarnya dibutuhkan scraper ala Prometheus |
/logs/* | nodes/log | Akses log, tetap tanpa exec |
/pods | nodes/pods, then nodes/proxy | Fallback ke nodes/proxy jika pengecekan sempit gagal |
/healthz | nodes/healthz, then nodes/proxy | Pengecekan ganda yang sama, fine-grained lebih dulu |
/exec/* and all others | nodes/proxy | Sisanya, termasuk exec, tetap butuh nodes/proxy |
# Wrong: what most monitoring charts shipped. "get" on nodes/proxy is not
# read-only, because a Websocket handshake is an HTTP GET to the kubelet.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring-agent
rules:
- apiGroups: [""]
resources: ["nodes/proxy"]
verbs: ["get"]
---
# Right: only what a scraper reads. Nothing here can exec into a container.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring-agent
rules:
- apiGroups: [""]
resources: ["nodes/metrics", "nodes/stats"]
verbs: ["get"]
# Find the ClusterRoles that still grant nodes/proxy (or a wildcard that
# includes it). Built-in admin roles will show up; read the rest.
kubectl get clusterroles -o json | jq -r '
.items[]
| select(any(.rules[]?; any((.resources // [])[];
. == "nodes/proxy" or . == "nodes/*" or . == "*")))
| .metadata.name'
# Confirm the gate is on, from a pod whose ServiceAccount may read nodes/metrics.
# The docs print: kubernetes_feature_enabled{name="KubeletFineGrainedAuthz",stage="GA"} 1
curl -sk -H "Authorization: Bearer $TOKEN" https://$NODE_IP:10250/metrics \
| grep kubernetes_feature_enabled | grep KubeletFineGrainedAuthzDocs juga mencatat bahwa ClusterRole bawaan system:kubelet-api-admin diperbarui untuk mencakup subresource baru, sehingga kubelet client milik API server tidak terpengaruh apa pun status gate-nya. Post Kubernetes menyebut arah jangka panjangnya adalah adopsi ekosistem, ketika agen seperti Prometheus dan Datadog menyediakan RBAC default yang lebih sempit, dan policy engine menandai binding nodes/proxy saat alternatif fine-grained tersedia. Itu prediksi mereka, bukan sesuatu yang saya verifikasi di chart mana pun.
Grant sempit yang ditambahkan di samping grant nodes/proxy lama tidak menghapus apa pun. Kubelet mengecek subresource sempit dulu lalu fallback ke nodes/proxy, jadi agen yang terkompromi dan masih memegang nodes/proxy tetap bisa exec. Risikonya baru turun saat grant lama dihapus.
User namespaces: root di container tidak lagi berarti root di node
User namespaces masuk GA di 1.36, dan docs mencantumkan gate UserNamespacesSupport pertama tersedia di v1.28. Masalah yang diselesaikannya dinyatakan di blog Kubernetes: proses yang berjalan sebagai root di dalam container dilihat kernel sebagai root di host, sehingga breakout lewat bug kernel atau mount yang salah mendarat sebagai root. Dengan hostUsers bernilai false, container tetap merasa dirinya root, jadi tool seperti apt dan dnf tetap jalan, tetapi host melihat user tanpa privilege, dan kubelet memilih mapping yang tidak dipakai pod lain di node itu.
Docs menyebut beberapa kerentanan berperingkat HIGH atau CRITICAL tidak bisa dieksploitasi saat user namespaces aktif, dan capability seperti CAP_SYS_ADMIN dibatasi pada user namespace milik pod. Bagian yang memakan waktu bertahun-tahun adalah kepemilikan volume: ID-mapped mounts, yang diperkenalkan di Linux 5.12, membiarkan kernel memetakan ulang kepemilikan saat mount alih-alih kubelet melakukan chown rekursif pada volume besar. Mount itulah alasan kebutuhan node di bawah ini seperti adanya.
| Komponen | Minimum menurut docs | Kenapa penting |
|---|---|---|
| Linux kernel | 6.3 | tmpfs mendapat dukungan idmap mount di 6.3, dan token service account serta Secret memakai tmpfs |
| runc | 1.2 | OCI runtime harus mendukung user namespaces |
| crun | 1.9 (1.13+ advised) | Docs menyarankan versi 1.13 atau lebih baru |
| containerd | 2.0 | CRI runtime juga harus mendukungnya |
| CRI-O | 1.25 | CRI runtime lain yang didukung di docs |
Sisi pod hanya satu field. Sisi node punya satu jebakan yang perlu diketahui sebelum Anda mengaktifkannya luas: secara default kubelet memberi pod ID di atas rentang 0 sampai 65535 agar tidak tumpang tindih dengan user host, dan hanya jika Anda ingin rentang kustom Anda butuh user kubelet, getsubids, dan file subordinate ID. Docs mewajibkan rentang dimulai dari kelipatan 65536 yang minimal 65536, dengan jumlah minimal 65536 dikali maxPods Anda.
# Pod side: one field. The container still sees uid 0, the host does not.
apiVersion: v1
kind: Pod
metadata:
name: isolated-workload
spec:
hostUsers: false # the opt-in; hostNetwork/hostPID/hostIPC must stay unset
containers:
- name: app
image: fedora:42
securityContext:
runAsUser: 0
# Node side, preflight. None of this is needed for the default ID range.
uname -r # needs 6.3+ for idmap mounts on tmpfs
runc --version # 1.2+, or: crun --version (1.9+)
containerd --version # 2.0+
# Only if you want a CUSTOM range: a user literally named "kubelet", and
# getsubids (shadow-utils) on the kubelet's PATH. Format is name:firstID:count.
# 65536 is the smallest legal start; 110 pods * 65536 = 7208960.
# /etc/subuid and /etc/subgid, one line each, identical:
kubelet:65536:7208960
# KubeletConfiguration, v1.33+: IDs per pod, a multiple of 65536 (default 65536).
userNamespaces:
idsPerPod: 1048576
# Verify: "id" inside the container says uid=0, but the process on the host
# (ps -o user= -p <host pid>) must NOT be root.
kubectl exec isolated-workload -- idDengan hostUsers bernilai false, capability seperti CAP_NET_ADMIN menjadi namespaced. Blog Kubernetes menyebut ini memungkinkan workload yang dulu butuh container privileged penuh, layak dilirik jika Anda menjalankan network tooling di pod.
gitRepo sudah hilang, dan migrasinya adalah init container
Tipe volume gitRepo sudah deprecated sejak v1.11. Di 1.36 plugin-nya dinonaktifkan permanen dan tidak bisa dihidupkan lagi, karena pemakaiannya bisa membiarkan penyerang menjalankan kode sebagai root di node. Docs volumes saat ini, yang ditulis untuk 1.37, menyatakan versi terakhir yang menyediakan cara memakai driver itu adalah v1.35, sejalan dengan itu: tidak ada flag untuk dibalik.
Release post dan docs volumes memberi pengganti yang sama: mount emptyDir ke init container yang meng-clone repository, lalu mount emptyDir yang sama ke container aplikasi. Release post juga menyebut tool bergaya git-sync untuk kasus ketika konten harus terus diperbarui. Biayanya nyata dan perlu disebut: gitRepo tidak butuh image sendiri, sedangkan init container butuh image yang berisi git dan keputusan soal credential.
Lakukan scan sebelum upgrade, karena Deployment yang pod template-nya masih mendeklarasikan gitRepo akan membuat pod yang gagal. Ekspresi ValidatingAdmissionPolicy di akhir blok diambil dari docs volumes dan menolak pemakaian baru.
# Before: removed in 1.36. A pod declaring this will not start.
volumes:
- name: src
gitRepo:
repository: https://git.example.com/team/site.git
revision: v1.4.0
# After: clone once in an init container, share through emptyDir.
spec:
volumes:
- name: src
emptyDir: {}
initContainers:
- name: clone
image: registry.example.com/tools/git:PINNED_TAG # your own pinned image
args: ["clone", "--depth=1", "--branch", "v1.4.0",
"https://git.example.com/team/site.git", "/src"]
volumeMounts:
- name: src
mountPath: /src
containers:
- name: app
image: registry.example.com/team/site:1.4.0
volumeMounts:
- name: src
mountPath: /srv/site
readOnly: true
# Find offenders in running objects AND controller templates. A scaled-to-zero
# Deployment has no pods but will fail the moment it scales up.
kubectl get pods,deploy,sts,ds,rs,job,cronjob -A -o json | jq -r '
.items[]
| select([.. | objects | select(has("gitRepo"))] | length > 0)
| .kind + " " + .metadata.namespace + "/" + .metadata.name'
# Block re-introduction: CEL for a ValidatingAdmissionPolicy, from the volumes docs.
!has(object.spec.volumes) || !object.spec.volumes.exists(v, has(v.gitRepo))externalIPs: deprecated sekarang, tiga langkah bertanggal
Field externalIPs pada Service adalah upaya awal memberi perilaku load balancer untuk cluster non-cloud. Blog Kubernetes menyebut API ini mengasumsikan setiap user di cluster sepenuhnya tepercaya, yang memungkinkan eksploitasi man-in-the-middle jika asumsi itu tidak berlaku, seperti dijelaskan di CVE-2020-8554. Proyek ini sudah menyarankan untuk menonaktifkannya sejak 1.21 dan menambahkan admission controller DenyServiceExternalIPs untuk itu, tetapi saat itu menilai pemblokiran default terlalu besar sebagai breaking change.
Blog memberi timeline kasar, dan perhatikan pilihan katanya: v1.40 paling cepat untuk kube-proxy berhenti mendukungnya dengan opsi opt-in kembali, lalu v1.43 paling cepat untuk penghapusan penuh tanpa jalan kembali. Release post menyebut penghapusan direncanakan di v1.43. Hanya field .spec.externalIPs yang terpengaruh, bukan tipe alamat Node bernama ExternalIP dan bukan kolom EXTERNAL-IP yang dicetak kubectl untuk Service LoadBalancer.
- 1.36: field dideprekasi, warning saat dipakai
- 1.40 paling cepat: dukungan kube-proxy dimatikan, dengan opsi opt-in untuk memulihkannya
- 1.43 paling cepat: dukungan dihapus total, tanpa opt-in
| Pengganti | Yang diberikan menurut post | Kekurangannya |
|---|---|---|
| LoadBalancer yang dikelola manual | Perilaku sama, tetapi IP berada di status, yang tidak bisa diedit user biasa dengan RBAC default | Post menyebutnya opsi termudah sekaligus terburuk: user yang diberi izin tetap bisa mereplikasi CVE-2020-8554, tanpa pengecekan bahwa satu user tidak mencuri IP user lain |
| Load balancer controller seperti MetalLB | Admin menentukan address pool dan controller mencegah dua Service memakai IP yang sama | Komponen baru untuk dijalankan, dan loadBalancerIP, yang masih didukung MetalLB, adalah field yang juga sudah deprecated |
| Gateway API | Resource Gateway membawa alamat, dan RBAC bisa membatasi siapa yang mengelola Gateway | Anda berpindah dari Service ke Gateway dan HTTPRoute, dan butuh implementasi Gateway terpasang |
# Inventory: every Service that still sets spec.externalIPs.
kubectl get svc -A -o json | jq -r '
.items[]
| select((.spec.externalIPs // []) | length > 0)
| .metadata.namespace + "/" + .metadata.name + " " + (.spec.externalIPs | join(","))'
# Block new use now. Append to your EXISTING plugin list, do not replace it.
# kube-apiserver flag:
--enable-admission-plugins=DenyServiceExternalIPs
# Replacement with MetalLB: the admin owns the pool, users just ask for a LoadBalancer.
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: production
namespace: metallb-system
spec:
addresses:
- 192.0.2.0/24
autoAssign: trueJika Anda tidak memakai field itu di mana pun, post menyatakan deprecation ini tidak berlaku untuk Anda, tetapi tetap menyarankan mengaktifkan DenyServiceExternalIPs sebagai pencegahan pemakaian di masa depan. Itu perubahan satu baris yang layak dilakukan di cluster tempat tenant bisa membuat Service.
Di mana ini gagal: lima failure mode yang diperingatkan docs
Tidak ada dari empat perubahan ini yang berbahaya sendirian. Kegagalan muncul dari celah antara perubahan itu dan cluster Anda, dan setiap poin di bawah dinyatakan di sumber, bukan hasil pengamatan saya.
- Menambah RBAC sempit tanpa menghapus nodes/proxy. Anda punya manifest yang lebih baik tetapi eksposur yang sama, karena fallback.
- Menyetel hostUsers false pada pod yang butuh hostNetwork, hostIPC, hostPID, atau raw block volumeDevices. Docs melarang keempat kombinasi itu.
- Mengaktifkan user namespaces di node tanpa dukungan filesystem. Filesystem node untuk direktori pod kubelet dan setiap volume yang di-mount pod harus mendukung idmap mounts, yang dalam praktik berarti Linux 6.3 atau lebih baru.
- Mengubah rentang subordinate ID di node yang sedang jalan. Docs meminta drain dulu, karena kubelet gagal start jika tidak bisa memenuhi konfigurasi baru untuk pod user-namespace yang sudah ada.
- Menganggap hostUsers false sebagai pengganti Pod Security. Kubernetes melonggarkan pengecekan runAsNonRoot dan runAsUser untuk pod semacam itu bahkan di Baseline dan Restricted, dengan alasan root di dalam tidak pernah dipetakan ke user host berprivilege. Alasannya masuk akal, tetapi artinya audit policy yang menghitung runAsNonRoot kini akan melewatkan pod terisolasi.
Trade-off yang jujur: user namespaces memberi defence in depth dengan harga syarat node dan jenis kegagalan scheduling baru, sedangkan tiga perubahan lain menuntut kerja migrasi dan langsung memberi blast radius yang lebih kecil.
gitRepo adalah satu-satunya perubahan di sini yang gagal secara terang-terangan pada hari upgrade. Jika scan pra-upgrade menemukan volume gitRepo di template Deployment, StatefulSet, atau CronJob, perbaiki lebih dulu. Pod yang sedang jalan bisa tidak muncul di scan khusus pod jika controller-nya sudah di-scale ke nol.
Yang dikerjakan hari Senin
Enam langkah, berurutan dari pengurangan risiko termurah. Script di bawah menjalankan audit read-only untuk langkah satu sampai tiga dan tidak mencetak apa pun yang destruktif.
- Jalankan script audit di setiap cluster sebelum upgrade apa pun, dan lampirkan outputnya ke tiket perubahan.
- Ganti setiap volume gitRepo dengan emptyDir plus init container, dan deploy perubahan itu di versi yang Anda pakai sekarang.
- Tambahkan DenyServiceExternalIPs ke admission plugin API server, lalu buat tiket untuk setiap Service dengan externalIPs yang ditemukan audit.
- Tulis ulang ClusterRole monitoring ke nodes/metrics, nodes/stats, dan nodes/pods, rollout, dan baru setelah itu hapus aturan nodes/proxy.
- Cek satu node pool terhadap syarat user namespaces, dan coba hostUsers false pada workload stateless di sana.
- Tambahkan ValidatingAdmissionPolicy untuk gitRepo agar tipe volume itu tidak bisa masuk lagi lewat chart usang.
#!/usr/bin/env bash
# k8s-136-audit.sh: read-only. Run per cluster, attach output to the change ticket.
set -euo pipefail
echo "== ClusterRoles granting nodes/proxy (built-ins included) =="
kubectl get clusterroles -o json | jq -r '
.items[]
| select(any(.rules[]?; any((.resources // [])[];
. == "nodes/proxy" or . == "nodes/*" or . == "*")))
| .metadata.name'
echo "== Objects declaring a gitRepo volume =="
kubectl get pods,deploy,sts,ds,rs,job,cronjob -A -o json | jq -r '
.items[]
| select([.. | objects | select(has("gitRepo"))] | length > 0)
| .kind + " " + .metadata.namespace + "/" + .metadata.name'
echo "== Services using spec.externalIPs =="
kubectl get svc -A -o json | jq -r '
.items[]
| select((.spec.externalIPs // []) | length > 0)
| .metadata.namespace + "/" + .metadata.name'
echo "== Node kernels (user namespaces need 6.3+) =="
kubectl get nodes -o custom-columns=NAME:.metadata.name,KERNEL:.status.nodeInfo.kernelVersion,RUNTIME:.status.nodeInfo.containerRuntimeVersionLangkah empat dan lima yang biasanya terlewat, karena tidak ada yang rusak jika dilewati. Itu justru alasan untuk menjadwalkannya.
Aturan yang saya bawa dari rilis ini: baca verb-nya, bukan namanya. Get pada nodes/proxy bertahun-tahun terdengar read-only dan ternyata bukan, user root di container terdengar terkurung dan ternyata bukan, dan field bernama externalIPs terdengar seperti plumbing padahal ia adalah asumsi kepercayaan. Kubernetes 1.36 memberi alat yang lebih sempit untuk masing-masing, tetapi hanya gitRepo yang memaksa migrasi. Kerjakan sisanya sesuai jadwal Anda, sebelum insiden orang lain yang menentukan jadwalnya.
Sumber dan bacaan lanjutan
- Blog Kubernetes: pengumuman rilis Kubernetes v1.36 (Haru)
- Blog Kubernetes: sneak peek v1.36, removal dan deprecation
- Blog Kubernetes: User Namespaces in Kubernetes are finally GA
- Blog Kubernetes: Fine-Grained Kubelet API Authorization graduates to GA
- Blog Kubernetes: Deprecation and removal of Service ExternalIPs
- Docs Kubernetes: User Namespaces
- Docs Kubernetes: Kubelet authentication and authorization
- Docs Kubernetes: Volumes, termasuk driver gitRepo yang dinonaktifkan

