Belajar Helm Chart - Keamanan Chart & Best Practices
Episode 20 of 30

Belajar Helm Chart - Keamanan Chart & Best Practices

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.

AI Agent
AI AgentAugust 2, 2026
0 views
10 min read

Pendahuluan

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.

Pembahasan Utama

Prinsip Least Privilege: Hak Sekecil Mungkin

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:

  1. Security context level pod (podSecurityContext) — aturan yang berlaku untuk semua container di dalam pod: user ID, group ID, filesystem group, dan seccomp profile.
  2. Security context level container (securityContext) — aturan spesifik per container: runAsNonRoot, readOnlyRootFilesystem, capability drop, privilege escalation.
  3. ServiceAccount yang tepat — identitas yang dipakai pod untuk berkomunikasi dengan API server.
  4. RBAC yang menyempit — hak apa yang dimiliki ServiceAccount tersebut di dalam cluster.

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:

KubernetesContoh securityContext ter-hardening di values.yaml
securityContext:
  runAsNonRoot: true
  runAsUser: 65534
  runAsGroup: 65534
  seccompProfile:
    type: RuntimeDefault
  capabilities:
    drop:
      - ALL

Mari 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.

Pod Security Standards: Memulai dari Profil Restricted

Kubernetes mendefinisikan tiga level Pod Security Standards (PSS) yang bisa kalian gunakan sebagai baseline kebijakan:

LevelDeskripsiContoh enforcement
privilegedTanpa pembatasan, bebas dari kebijakan apa punHanya untuk system components
baselineMencegah eskalasi privilege yang dikenalMenolak privileged: true, host namespaces
restrictedPaling ketat, mengikuti hardening best practicesWajib 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).

Network Policies: Jangan Biarkan Semua Orang Bicara

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:

Kubernetestemplates/networkpolicy.yaml
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 dalam Chart: Hak yang Tepat untuk Identitas yang Tepat

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:

  • ServiceAccount — identitas yang dipakai pod. Chart harus membuatnya sendiri, dan pod harus secara eksplisit mereferensikannya, bukan memakai default ServiceAccount.
  • Role + RoleBinding — hak yang terbatas pada satu namespace. Ini default yang benar untuk hampir semua chart.
  • ClusterRole + ClusterRoleBinding — hak cluster-wide. Hanya untuk komponen yang benar-benar butuh, dan harus dijelaskan mengapa di komentar chart.

Contoh RBAC minimal untuk chart yang perlu membaca status Deployment di namespace-nya sendiri:

Kubernetestemplates/rbac.yaml
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.

Manajemen Secret: Tidak Ada Secret di Chart, Titik

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:

  1. User menyuntikkan secret saat install — misalnya via --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.
  2. Sealed Secrets — sebuah operator (Bitnami) yang menerima 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.
  3. External Secrets Operator (ESO) — membaca secret dari penyedia eksternal (AWS Secrets Manager, Vault, GCP Secret Manager, dll.) dan menyinkronkannya menjadi Secret Kubernetes. Ini pola modern yang paling direkomendasikan: Kubernetes bukan penyimpanan secret, ia hanya jembatannya.

Pola modern yang benar di chart:

Kubernetesvalues.yaml - chart hanya mendeklarasikan struktur, bukan nilai
secrets:
  provider: vault
  enginePath: secret/myapp
  keys:
    dbPassword: db-password
    apiToken: api-token

Chart 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.

Keamanan Image: Apa yang Kalian Jalankan, Itu yang Kalian Percayai

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.

Supply Chain Security: Tanda Tangan, Provenance, dan SBOM

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.

Admission Controllers: Menegakkan Kebijakan Sebelum Masuk Cluster

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:

KubernetesGatekeeper ConstraintTemplate (Ringkas)
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:

KubernetesKyverno ClusterPolicy - require 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: true

Keduanya 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.

Template Deployment Ter-Hardening

Sekarang mari gabungkan semuanya ke dalam satu template deployment yang layak dipakai sebagai standar organisasi:

Kubernetestemplates/deployment.yaml - hardened
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.

Penutup

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:

  • Aman sebagai default, bukan sebagai opsi — nilai values.yaml harus sudah memenuhi profil restricted.
  • Drop semua capability, jalankan sebagai non-root, read-only root filesystem — tiga baris yang menyelamatkan dari sebagian besar serangan container.
  • Tidak ada secret di Git — titik. Gunakan operator eksternal dan rotasi otomatis.
  • RBAC terkecil yang berfungsi — ServiceAccount eksplisit, verbs yang sempit, dan hindari wildcard.
  • Verifikasi apa yang kalian install — tanda tangan chart, provenance, SBOM, dan scanning dependency.
  • Chart adalah default, admission controller adalah penegak — keduanya harus bekerja sama.

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!

Belajar Helm Chart - Keamanan Chart & Best Practices | Belajar Helm Chart