Mengamankan chart dari fondasi: least privilege, security context, Pod Security Standards, network policies, RBAC yang benar, manajemen secret tanpa hardcode, keamanan image, supply chain (signing, provenance, SBOM), hingga policy enforcement dengan OPA/Gatekeeper dan Kyverno.

Setelah di episode 19 sebelumnya kita membahas library charts — cara memisahkan logika templating yang reusable agar semua chart di organisasi memakai satu sumber kebenaran — pada episode kali ini kita naik satu tingkat lebih penting: keamanan. Kalian bisa punya chart yang rapi, teruji, dan terdokumentasi sempurna, tapi jika chart tersebut mendeploy aplikasi dengan privilege berlebihan, secret yang tertanam di Git, dan image yang rentan, maka semua kerja keras itu hanya menciptakan permukaan serangan yang indah untuk dieksploitasi.
Keamanan bukan fitur yang dipasang di akhir — ia adalah properti desain. Bayangkan chart kalian sebagai apartemen: tembok dan catnya (templating dan struktur) bisa bagus, tapi kalau semua penghuni berbagi satu kunci master yang sama, pintunya selalu terbuka, dan brankas rahasia ditaruh di lobi depan, maka keindahan apartemen tidak ada artinya. Di dunia Kubernetes, "kunci master" itu adalah container yang berjalan sebagai root, "pintu terbuka" adalah service yang bisa diakses siapa saja di cluster, dan "brankas di lobi" adalah secret yang ditulis polos di values.yaml.
Dan ancamannya nyata: sebagian besar breach di Kubernetes bukan berasal dari eksploitasi bug kernel yang canggih, melainkan dari miskonfigurasi — container root, secret ke-commit di Git, image lama yang rentan. Karena konfigurasi itulah yang lahir dari chart, chart kalian adalah baseline keamanan organisasi besok.
Pada episode ini kita akan membedah enam lapisan: least privilege dan security context, Pod Security Standards, RBAC, manajemen secret, keamanan image, supply chain security, dan policy enforcement lewat admission controllers — lalu menutupnya dengan template deployment ter-hardening yang bisa kalian jadikan standar.
Least privilege adalah prinsip yang menyatakan: setiap komponen — proses, user, service — hanya diberi hak yang benar-benar dibutuhkan untuk menjalankan fungsinya, tidak lebih. Di Kubernetes, prinsip ini diterjemahkan ke beberapa mekanisme yang berbeda, dan chart kalian harus menghormati semuanya sekaligus:
podSecurityContext) — aturan yang berlaku untuk semua container di dalam pod: user ID, group ID, filesystem group, dan seccomp profile.securityContext) — aturan spesifik per container: runAsNonRoot, readOnlyRootFilesystem, capability drop, privilege escalation.Jebakan paling umum di chart default adalah menjalankan container sebagai root dan membiarkan semua capability CAP_* aktif. Container root yang dieksploitasi memberi penyerang kontrol penuh atas proses di dalamnya — dan dengan namespaces yang longgar, bisa menyerang host. Solusi minimal untuk setiap chart production:
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
seccompProfile:
type: RuntimeDefault
capabilities:
drop:
- ALLMari bedah satu per satu. runAsNonRoot: true memaksa Kubernetes menolak container yang mencoba berjalan sebagai user ID 0 (root) — ini lapisan pertahanan terhadap image yang kurang terawat. runAsUser: 65534 adalah user nobody — user tanpa privilege sama sekali yang eksis di hampir semua distribusi Linux. seccompProfile: RuntimeDefault mengaktifkan profil seccomp bawaan container runtime yang memblokir ratusan syscall berbahaya. Dan capabilities.drop: ["ALL"] mencabut seluruh capability Linux dari container — sebuah kontainer aplikasi hampir tidak pernah butuh CAP_NET_RAW atau CAP_SYS_PTRACE. Prinsipnya: drop semua, tambahkan hanya yang terbukti dibutuhkan.
Kubernetes mendefinisikan tiga level Pod Security Standards (PSS) yang bisa kalian gunakan sebagai baseline kebijakan:
| Level | Deskripsi | Contoh enforcement |
|---|---|---|
privileged | Tanpa pembatasan, bebas dari kebijakan apa pun | Hanya untuk system components |
baseline | Mencegah eskalasi privilege yang dikenal | Menolak privileged: true, host namespaces |
restricted | Paling ketat, mengikuti hardening best practices | Wajib runAsNonRoot, drop ALL, seccomp |
Level restricted adalah target yang paling masuk akal untuk chart aplikasi bisnis. Pod yang memenuhi profil ini harus: berjalan sebagai non-root, tidak memakai host namespaces, tidak memakai host ports, mencabut semua capabilities, read-only root filesystem, dan seccomp diaktifkan. Menariknya — kebanyakan syarat restricted adalah hal yang justru sudah kita pasang di security context contoh tadi. Ini bukan kebetulan: PSS dirumuskan dari praktik hardening yang sama.
Konsekuensi praktis untuk penulis chart: nilai default security context di values.yaml kalian harus sudah memenuhi profil restricted. Jika user harus "mengingat" untuk mengaktifkan runAsNonRoot, maka mayoritas install akan lupa. Jadikan aman sebagai default, dan berikan jalan keluar yang eksplisit — bukan default — untuk workload yang memang butuh privilege (misalnya DaemonSet yang membaca kernel).
Default Kubernetes adalah allow-all: setiap pod bisa berbicara dengan pod lain di cluster mana pun. Untuk chart yang men-deploy service internal seperti database, ini berarti database kalian bisa diakses dari namespace lain yang tidak berkepentingan. Network Policies mengubah aturan ini menjadi allowlist — mirip firewall per-pod.
Network Policy adalah resource yang paling sering dilupakan di chart. Contoh untuk chart yang hanya boleh diakses dari service gateway:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: gateway
ports:
- protocol: TCP
port: {{ .Values.service.port }}Perhatikan dua hal. Pertama, policy ini kosong untuk Egress — hanya membatasi Ingress. Ini pilihan yang sadar: membatasi ingress adalah langkah paling bernilai, sementara egress yang salah konfigurasi bisa memutus DNS atau traffic ke database eksternal. Kedua, resource ini sering dibuat kondisional — banyak tim menyediakan flag networkPolicy.enabled. Tapi pertanyaannya harusnya dibalik: mengapa default-nya matikan standar keamanan?
RBAC (Role-Based Access Control) di Kubernetes adalah sistem yang menentukan siapa (ServiceAccount) bisa apa (permissions) di mana (namespaces atau cluster-wide). Sebuah chart jarang membutuhkan RBAC untuk aplikasi biasa — aplikasi web yang hanya menerima request HTTP tidak butuh hak membaca Secret di cluster. Tapi untuk chart infrastruktur — seperti exporter, operator, atau controller — RBAC adalah kebutuhan wajib, dan berikut polanya:
default ServiceAccount.Contoh RBAC minimal untuk chart yang perlu membaca status Deployment di namespace-nya sendiri:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "myapp.fullname" . }}
namespace: {{ .Release.Namespace }}
rules:
- apiGroups: [""]
resources: ["pods", "services"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "myapp.fullname" . }}
namespace: {{ .Release.Namespace }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "myapp.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "myapp.fullname" . }}
namespace: {{ .Release.Namespace }}Perhatikan verbs yang disempitkan ke ["get", "list", "watch"] — bukan ["*"]. Prinsip di balik ini: grant permission paling kecil yang masih membuat sistem berfungsi. RBAC adalah salah satu area paling rentan di Kubernetes karena banyak chart memakai wildcard untuk "supaya aman" — ironisnya, itu justru membuang keamanan.
Important
Waspadai helm.sh/resource-policy: keep. Saat release di-uninstall, Helm menghapus semua resource yang ia buat. Anotasi helm.sh/resource-policy: keep memberitahu Helm untuk tidak menghapus resource tersebut saat uninstall atau upgrade — misalnya PersistentVolumeClaim yang datanya harus diselamatkan. Gunakan sangat hemat: jika resource di-keep, maka saat helm upgrade berjalan dan release berikutnya mencoba membuat resource dengan nama sama, install akan gagal karena resource lama masih ada — kalian harus menghapusnya manual. Karena alasan inilah banyak tim melarang resource-policy: keep di chart aplikasi dan hanya mengizinkannya di chart data yang benar-benar butuh persistensi. Jangan pernah menaruhnya di Secret atau ConfigMap tanpa alasan yang sangat kuat.
Aturan pertama dan terpenting: chart tidak boleh berisi secret. values.yaml adalah file yang di-commit ke Git, direview, di-branch, dan di-sync ke banyak environment. Segala sesuatu yang masuk ke sana akan selamanya ada di riwayat Git. Secret yang tersimpan di values.yaml atau — lebih buruk — di template sebagai literal adalah kebocoran yang menunggu waktu.
Lalu dari mana secret datang? Tiga pola yang benar:
--set-string atau file values terpisah yang tidak di-commit. Chart cukup mendeklarasikan required atau lookup untuk meminta nilainya. Ini pola paling sederhana, tapi nilainya tetap hidup di riwayat helm dan bisa bocor lewat helm get values.SealedSecret (aman untuk di-commit) dan men-decrypt-nya menjadi Secret biasa di cluster. Kunci enkripsinya hidup di cluster, bukan di Git. Dengan ini, Git berisi artefak yang bisa di-commit dengan aman.Pola modern yang benar di chart:
secrets:
provider: vault
enginePath: secret/myapp
keys:
dbPassword: db-password
apiToken: api-tokenChart kemudian membuat resource ExternalSecret atau SealedSecret berdasarkan struktur itu, dan tidak pernah menampilkan nilainya. Pola ini juga menjawab rotasi secret: saat secret berubah di penyedia, operator menyinkronkannya, dan Pod yang membaca Secret hanya butuh restart untuk mengambil nilai baru. Rotasi manual bulanan pun menjadi otomatis dan terpusat.
Chart mendeploy image — dan image adalah produk dari seluruh supply chain perangkat lunak. Empat hal yang harus diatur chart:
Image pull policy. imagePullPolicy: IfNotPresent memakai image lokal jika ada — berbahaya di cluster multi-node karena node bisa menyimpan image basi. Always memaksa menarik dari registry setiap pod dibuat, menjamin versi konsisten di semua node; untuk chart production ini pilihan yang jauh lebih aman. Dan jangan pernah memakai tag latest — tag mutable yang bisa berubah tanpa sepengetahuan siapa pun; pin ke digest (sha256:...) jika ketat, atau minimal ke tag versi yang immutable.
Private registry. Image internal harus ditarik dari registry privat. Chart perlu mendeklarasikan imagePullSecrets agar kubelet bisa autentikasi — dan itu adalah Secret yang harus dibuat di luar chart (misalnya oleh pipeline atau operator), karena credential-nya tidak boleh ada di Git. Chart cukup mereferensikan nama secret-nya.
Image scanning. Setiap image yang dideploy chart harus melewati pemindaian kerentanan — trivy image untuk CLI, atau scanning terintegrasi di registry (GHCR, ECR, Artifact Registry). Chart tidak bisa menegakkan ini sendiri, tapi sebaiknya menyimpan annotations yang mencatat hash manifest dan metadata hasil scan agar auditable.
Distroless images. Image yang berisi distro lengkap (apt, bash, tooling) membawa ratusan binary yang tidak dipakai — dan setiap binary adalah permukaan serangan. Image distroless (dari Google, misalnya gcr.io/distroless/static) hanya berisi runtime yang dibutuhkan aplikasi: tanpa shell, tanpa package manager, tanpa utilitas. Penyerang yang berhasil masuk ke container distroless tidak punya shell untuk di-exploit — permukaan serangan menyusut drastis. Kombinasi ideal: distroless + runAsNonRoot + drop all capabilities.
Chart yang kalian install bisa saja telah di-tamper di tengah jalan — repository di-klaim, package disisipkan malware, atau dependency yang tampak sah ternyata berubah. Supply chain security menjawab pertanyaan: "apakah artefak yang kalian terima benar-benar yang dibuat oleh maintainer?"
Chart signing & provenance. Helm mendukung penandatanganan chart dengan GPG. Saat packaging, helm package --sign --key 'nama-kunci' menghasilkan file .tgz plus file .prov (provenance) yang berisi tanda tangan dan manifest checksum. Verifikasi saat install: helm install --verify. Prosesnya: Helm menghitung checksum file .tgz, membandingkan dengan yang tercatat di .prov, lalu memvalidasi tanda tangan GPG terhadap kunci publik yang kalian percayai. Ini menjawab dua masalah sekaligus — integritas (file tidak berubah) dan otentikasi (berasal dari maintainer yang benar).
Dependency scanning. Chart modern memiliki dependencies — dan masing-masing bisa membawa celah. helm dependency list menampilkan versi, tapi kalian perlu memindainya secara berkala terhadap database CVE, dan memastikan update dependency masuk lewat pull request yang direview — bukan helm dependency update yang dijalankan diam-diam di laptop.
SBOM (Software Bill of Materials). Untuk chart production, tim keamanan biasanya meminta SBOM — daftar lengkap komponen yang menyusun artefak. syft scan menghasilkan SBOM, cosign attest menandatanganinya, dan hasilnya disimpan di registry agar siapa pun bisa memverifikasi "chart ini terdiri dari komponen X, Y, Z pada versi tertentu". Cosign juga dipakai untuk menandatangani chart OCI di registry — standar modern yang menggantikan GPG untuk artefak OCI.
Security context di chart kalian hanyalah default — tidak menjamin semua orang mematuhinya. Yang menegakkan kebijakan secara keras di level cluster adalah admission controllers, khususnya dua yang paling populer:
OPA/Gatekeeper menggunakan kebijakan yang ditulis dalam Rego. Sebuah constraint template mendefinisikan aturan — misalnya "semua pod harus punya runAsNonRoot: true" — lalu constraint diterapkan ke namespace tertentu. Contoh constraint yang menolak pod tanpa runAsNonRoot:
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.securityContext
msg := sprintf("container %v must set securityContext", [container.name])
}Kyverno menawarkan pendekatan yang lebih mudah didekati: kebijakan ditulis dalam YAML murni, bukan bahasa pemrograman. Kebijakan Kyverno yang setara — memaksa semua pod di namespace prod berjalan non-root:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-non-root
spec:
validationFailureAction: Enforce
rules:
- name: check-runasnonroot
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Pod harus memiliki securityContext.runAsNonRoot: true"
anyPattern:
- spec:
securityContext:
runAsNonRoot: true
- spec:
containers:
- securityContext:
runAsNonRoot: trueKeduanya bekerja di level yang sama — menolak resource yang melanggar sebelum masuk cluster — dengan trade-off: Gatekeeper lebih fleksibel (Rego Turing-complete) tapi kurva belajarnya curam; Kyverno lebih sederhana dan cocok untuk sebagian besar kasus. Untuk penulis chart: chart yang baik dan kebijakan yang menegakkan standar saling melengkapi — chart menyediakan default aman, admission controller memaksa orang untuk tidak menimpanya.
Sekarang mari gabungkan semuanya ke dalam satu template deployment yang layak dipakai sebagai standar organisasi:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "myapp.fullname" . }}
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
{{- toYaml .Values.resources | nindent 12 }}Perhatikan dua baris yang sering dilupakan. automountServiceAccountToken: false mencabut akses pod ke API server — aplikasi biasa tidak butuh token ServiceAccount, dan token adalah salah satu vektor pencurian kredensial yang paling umum. Dan readOnlyRootFilesystem: true membuat satu-satunya filesystem yang bisa ditulis adalah volume yang ter-mount — jika aplikasi tidak butuh menulis file di container, jangan izinkan.
Pada episode 20 ini kita telah membangun lapisan demi lapisan keamanan chart: least privilege lewat security context yang menolak root dan mencabut capability, Pod Security Standards dengan profil restricted sebagai baseline, network policies yang mengubah default allow-all menjadi allowlist, RBAC yang menyempitkan hak ServiceAccount, manajemen secret yang mengusir semua secret dari Git dan mengalihkannya ke Sealed Secrets atau External Secrets Operator, keamanan image dengan pull policy dan distroless, supply chain security dengan signing, provenance, dan SBOM, serta admission controllers yang menegakkan kebijakan di level cluster.
Inti yang harus kalian bawa:
values.yaml harus sudah memenuhi profil restricted.verbs yang sempit, dan hindari wildcard.Di episode 21 selanjutnya kita akan membahas multi-environment management: bagaimana mengatur chart yang sama untuk dev, staging, dan production tanpa membuat tiga chart berbeda — mulai dari organisasi file values, promotion pipeline, hingga Helmfile sebagai pendeklarasi release secara menyeluruh. Pastikan fondasi keamanan dari episode ini sudah kalian pahami, karena di episode berikutnya semua pola itu akan dipraktekkan di banyak environment sekaligus. Sampai jumpa di episode 21!