Mengamankan workload kontainer end-to-end: image minimal yang discan dengan Trivy, Pod Security Standards, RBAC least privilege, admission control dengan Kyverno, network policy, dan secret management di Kubernetes, dirangkum menjadi checklist hardening cluster produksi

Setelah di episode 11 kita membangun pipeline IaC security yang mencegah misconfigurasi lahir di pull request, sekarang kita terjemahkan prinsip-prinsip itu ke unit deployment modern: kontainer dan Kubernetes. Perimeter IAM dan network dari episode 3-4 harus punya padanan langsung di dalam cluster.
Mengapa episode ini penting? Karena default Kubernetes terlalu permisif untuk produksi: pod boleh jalan sebagai root, semua namespace bisa saling bicara, dan siapa pun yang punya izin create pod praktis bisa mencuri service account tetangganya. Hardening K8s adalah pekerjaan sistematis — dan karena semuanya deklaratif, ia sangat cocok untuk engineer seperti kalian.
Kerangka mental yang rapi untuk seluruh episode:
Permukaan serangan sebuah image proporsional dengan jumlah package di dalamnya. Ganti base image gemuk dengan distroless:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 curl
COPY . /app
CMD ["python3", "/app/server.py"]Distroless bahkan tidak punya shell — reverse shell dan tool-download mati begitu saja. Lalu scan setiap build:
trivy image --exit-code 1 --severity CRITICAL,HIGH registry.acme.io/app@sha256:<digest>
trivy image --ignore-unfixed registry.acme.io/app:latestPin by digest (@sha256:...) agar tag tidak berubah diam-diam, dan gunakan --ignore-unfixed untuk menekan noise vulnerability tanpa fix.
Pod Security Standards (PSS) adalah kontrol bawaan Kubernetes di level namespace:
| Level | Efek | Cocok Untuk |
|---|---|---|
| privileged | Tanpa batasan | Hanya namespace sistem |
| baseline | Blok kemampuan berbahaya umum | Workload umum |
| restricted | Plus runAsNonRoot, drop capabilities, seccomp | Produksi ketat |
Terapkan level restricted di namespace produksi:
kubectl label ns production pod-security.kubernetes.io/enforce=restrictedManifest yang lolos level restricted berpola seperti ini:
spec:
template:
spec:
securityContext:
runAsNonRoot: true
seccompProfile: {type: RuntimeDefault}
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: [ALL]}
volumeMounts:
- {name: tmp, mountPath: /tmp}
volumes:
- name: tmp
emptyDir: {}readOnlyRootFilesystem layak digarisbawahi: malware di container tak bisa menulis payload ke disk — sering satu setting ini yang memutus persistence. Aplikasi yang butuh direktori tulis diberi emptyDir spesifik seperti contoh di atas.
RBAC Kubernetes bekerja lewat dua objek: Role/ClusterRole (izin apa) dan RoleBinding (siapa). Prinsipnya identik dengan episode 3 — role over user, least privilege:
kind: Role
metadata:
name: app-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
---
kind: RoleBinding
metadata:
name: app-reader-binding
namespace: production
subjects:
- kind: ServiceAccount
name: app
namespace: production
roleRef:
kind: Role
name: app-reader
apiGroup: rbac.authorization.k8s.ioDua audit cepat untuk dilakukan rutin:
kubectl get clusterrolebindings -o wide
kubectl get pods -A -o custom-columns='NS:.metadata.namespace,SA:.spec.serviceAccountName' | sort -uPod yang tidak bicara ke API server wajib memakai automountServiceAccountToken: false — token yang tidak ada tidak bisa dicuri. Untuk aturan organisasi (registry internal saja, wajib label, verifikasi signature cosign), pasang admission controller seperti Kyverno:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-trusted-registry
spec:
validationFailureAction: Enforce
rules:
- name: trusted-registries
match:
any:
- resources: {kinds: [Pod]}
validate:
message: "Image hanya boleh dari registry.acme.io"
pattern:
spec:
containers:
- image: "registry.acme.io/*"Dengan Kyverno mode Enforce, pod dari registry asing ditolak di admission — sebelum pernah jalan.
Secara default, semua pod saling bisa bicara lintas namespace. NetworkPolicy mengubah default deny menjadi eksplisit:
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: api-default-deny
namespace: production
spec:
podSelector:
matchLabels: {app: api}
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels: {name: ingress}
ports:
- {protocol: TCP, port: 8080}Pola rollout yang aman: mulai dengan policy audit-only (label namespace + monitoring), lihat trafik nyata, lalu enforce. Tanpa tahap observasi dulu, default deny bisa memutus dependensi yang tidak terdokumentasi.
Tip
Di EKS/GKE, pastikan network policy engine-nya aktif (VPC CNI network policy / Dataplane V2). Manifest NetworkPolicy di cluster tanpa enforcement hanyalah komentar YAML — tampak rapi, tanpa efek.
Secret bawaan K8s hanyalah base64 — bukan enkripsi. Dua peningkatan wajib:
EncryptionConfiguration dengan provider KMS) agar secret di etcd tidak bisa dibaca dari backup.apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: api-db-password
namespace: production
spec:
refreshInterval: 1h
secretStoreRef: {name: aws-secrets-manager, kind: ClusterSecretStore}
target: {name: api-db-password}
data:
- secretKey: password
remoteRef: {key: prod/api/db, property: password}Detail strategi secrets lintas-platform kita perdalam di episode 14.
Rangkuman produksi yang bisa langsung dipakai:
restricted di namespace workload; privileged hanya namespace sistem.Inti yang harus dibawa pulang:
Di episode 13 selanjutnya kita beralih ke model deployment tanpa server: serverless & function security — permission Lambda, environment variable vs secrets manager, dan auth function URL. Sampai jumpa!