Docker Container Security: A Practical Guide
Panduan praktis keamanan container Docker mulai dari image security, runtime hardening, network segmentation, hingga secrets management untuk lingkungan produksi.
Docker Container Security: A Practical Guide
Gue udah cukup lama ngelatih container di production, dan honestly yang bikin bete bukan cara deploynya — tapi harus nerapin securitynya. Banyak orang ngira container itu "securely isolated by default" karena ada namespace dan cgroup, padahal kenyataannya banyak celah yang biasa dilupain. Gue mau cerita dari pengalaman, bukan dari teori buku. Percayain, yang lo lakuin sehari-hari — image builder, runtime config, secrets management — itu yang bikin bedanya antara cluster yang aman sama yang cuma sabar doang sebelum kena exploit.
Kenapa Container Bukan Sekadar "Mini VM"
Banyak yang masih ngira container itu mirip VM, cuma lebih ringan. Beda. VM punya kernel masing-masing; container sharing kernel dengan host. Itu yang bikin attack surface-nya beda. Kalau ada vulnerability di kernel, container cuma bantingan pintu, bukan tembok beton. Gue ingat kali pertama lo pegang orchestration, lo liat pod bisa akses node filesystem dan langsung wow, baru sadar seberapa besar blast radius kalau salah config.
Nah, dari situ lo mulai ngerti: container isolation itu lapisan, bukan satu lapis. Ada namespace, ada cgroup, ada seccomp, ada AppArmor/SELinux, ada capabilities. Kalau lo lepasin satu, lainnya ikutan nggak aman. Jadi pendekatan yang bener itu defense in depth, bukan cuma nyalain --privileged terus berharap apapun yang terjadi bisa tanggung.
Image Security: Dari Source Hingga Push
Yang paling awal dan paling sering dilupain itu image. Gue sering liat tim build image dari base image latest, pull dependency sembarang, tanpa scan. Hasilnya? Di production ada CVE yang udah disclosed 6 bulan yang lalu, dan imagenya masih kepake.
Yang pertama, pake pinned base image. Jangan ever pake :latest. Lo harus tahu exact version yang lo pake, misal python:3.11-slim-bookworm bukan cuma python:3.11. Kenapa? Karena kalau lo rebuild besok tanpa cache, base image bisa berubah total — muncul package baru yang belum di-audit, atau worse, compromised image di registry.
Kedua, minimalize image layers dan install cuma yang dibutuhkan. Jangan install vim, curl, dan git di production image kecuali lo benar-benar butuh di runtime. Setiap binary yang ada di image itu potensi exploit. Kalau ada vulnerability di curl, lo cuma butuh curl di build stage, bukan di final stage.
Contoh multi-stage build yang bener gue sering pake:
# Build stage
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production=false
COPY . .
RUN npm run build
# Production stage
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser
EXPOSE 3000
CMD ["node", "dist/main.js"]Di sini lo bisa liat gue pake non-root user di final stage. Itu penting. Kalau container lo kena RCE tapi running as root, attacker langsung punya akses ke node (kalo --privileged atau capability yang lepas). Kalau cuma sebagai user appuser dengan shell /sbin/nologin, attacker lebih sulit pindah.
Ketiga, pake .dockerignore. Banyak orang build Docker tanpa .dockerignore, jadi seluruh project directory — termasuk .env, .git, dan dokumentasi internal — masuk ke image. Lo cuma butuh file yang dibutuhin buat run app. Git history bisa bocorkan credentials atau internal architecture yang sebenernya nggak perlu ada di image.
Keempat, scan image sebelum push. Lo bisa pake Trivy, Grype, atau Snyk. Gue paling sering pake Trivy karena cuma butuh satu binary, nggak perlu daemon. Scan di CI pipeline, block push kalau ada CVE critical yang belum diperbaiki. Jangan biar security scan cuma jadi "nice to have" yang di-skip pas deadline.
Trivy config yang gue pake di CI:
trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:${COMMIT_SHA}Kalau exit code 1, pipeline gagal. Simple. Nggak perlu dashboard yang rumit. Yang penting critical vulnerability nggak masuk production.
Runtime Security: Yang Banyak Dilupain
Ini yang bikin gue paling sering ngeyel. Lo udah bikin image yang bersih, tapi runtimenya salah config — celahnya tetap ada.
Privileged Mode Jangan Pernah
Jangan pernah pakai --privileged kecuali lo benar-benar tahu apa yang lo lakuin dan udah punya mitigasi lain. Privileged container itu basically masuk ke host. Lo bisa akses semua device, bisa mount host filesystem, bisa ubah kernel parameter. Banyak tutorial online yang suruh pake privileged buat Kubernetes pod yang butuh akses hardware — itu bad practice. Lo bisa pake capability yang lebih spesifik, misal SYS_ADMIN untuk mounting, tapi itu juga nggak aman. Gue pernah lihat pod yang cuma butuh akses serial device, tapi tim deploy pake --privileged karena males cari capability yang pas.
Capabilities yang Wajib Dikurangi
Secara default container punya capabilities yang terlalu banyak. Yang gue usually revoke:
NET_RAW— bisa buat packet spoofing dan network scanning dari dalam containerSYS_ADMIN— powerful banget, bisa mount filesystem, ubah kernel parameterSYS_PTRACE— bisa debug process lain, termasuk container lainNET_ADMIN— bisa ubah routing, firewall rules
Di Kubernetes lo bisa pake securityContext.capabilities.drop: ["ALL"] terus tambahin yang lo butuh aja. Itu jauh lebih aman daripada revoke satu per-satu.
Seccomp dan AppArmor
Seccomp (secure computing mode) bisa filter syscall yang boleh dijalankan container. Kalau container lo cuma butuh read, write, open, close, jangan izinkan execve atau ptrace. Kubernetes udah punya seccomp profile default sejak 1.22 yang cukup ketat. Pake itu dulu sebelum bikin custom profile.
AppArmor/SELinux itu lapisan lain. Gue biasanya enable AppArmor profile yang ada di Docker default. Kalau butuh custom, gue mulai dari yang udah ada (misal docker-default) lalu modify perlahan. Jangan mulai dari blank — terlalu ribet dan rawan miss config.
Read-Only Root Filesystem
Ini yang paling mudah dilakuin tapi paling sering dilupain. Lo tinggal tambah --read-only pas run container, terus mount volume temporary di /tmp kalau app butuh write. Keuntungannya: attacker yang dapat RCE nggak bisa install backdoor, nggak bisa ubah binary, nggak bisa bikin cronjob. Blast radiusnya jadi kecil banget.
Di Kubernetes:
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: falseItu dua baris config yang bikin container lo jauh lebih resilient. Gue liat banyak incident di mana attacker bisa maintain access cuma karena container punya writable root filesystem.
Secrets Management: Jangan Pakai Environment Variable Kembar
Ini yang bikin gue bete banget. Banyak tim masih simpen database password, API key, sama certificate di environment variable atau di image. Padahal ada alternatif yang lebih aman.
Yang pertama, jangan pernah bake secrets ke image. Jangan pake ENV DB_PASSWORD=xxx di Dockerfile. Juga jangan COPY .env ke image. Environment variable itu cuma sedikit lebih aman daripada bake ke image — masih bisa liat lewat docker inspect atau env dari dalam container. Kalau ada attacker yang bisa akses node atau container runtime API, secretsnya ketahuan.
Yang kedua, pake secret manager. Kalau lo di Kubernetes, pake Vault, AWS Secrets Manager, atau GitLab CI variables yang di-mask. Di runtime, secret di-inject lewat volume atau environment variable yang di-handle oleh orchestrator, bukan di-bake di image.
Yang ketiga, kalau lo harus simpen secret di runner CI/CD, pastikan itu ephemeral. Setelah job selesai, secretnya dihapus. Jangan simpen di disk yang persistent.
Yang keempat, rotate secrets secara berkala. Jangan pake password yang sama selama 2 tahun. Kalau ada kebocoran, lo nggak tau kapan password itu bocor, jadi rotate rutin minimal setiap 90 hari untuk yang critical.
Network Segmentation: Jangan Semua Container Bisa Ke Mana Saja
Default Docker network itu bridge — semua container di network yang sama bisa saling talk tanpa restriction. Gue pernah lihat cluster dimana frontend container bisa langsung konek ke PostgreSQL port tanpa lewat API. Itu bahaya. Kalau frontend kena injection, attacker bisa langsung lanjut ke database.
Yang pertama, pake custom network untuk setiap tier. Misal network frontend, network backend, network database. Frontend cuma bisa akses backend, backend cuma bisa akses database. Jangan campur.
Kedua, gunakan network policy. Di Kubernetes itu NetworkPolicy. Di Docker Compose lo bisa pake internal: true untuk network yang nggak perlu akses internet.
Contoh NetworkPolicy sederhana buat backend yang cuma bisa diterima koneksi dari frontend:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-frontend
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080Ini bikin backend nggak bisa diterima koneksi dari manapun kecuali frontend pod yang punya label app: frontend. Kalau ada attacker yang manage compromise pod lain, mereka nggak bisa langsung akses backend.
Ketiga, disable jaringan antar container yang nggak perlu. Di Docker Compose lo bisa pake network_mode: bridge dengan network yang lebih ketat, atau di Kubernetes pake NetworkPolicy yang default deny all.
Logging dan Monitoring: Kapan Harus Curiga
Security bukan cuma preventif, harus punya detection. Di container environment, yang penting adalah:
- 1Audit container lifecycle events — create, start, stop, destroy, exec masuk container
- 2Monitor network connection yang aneh — container yang tiba-tiba konek ke IP yang belum pernah dikunjungi
- 3Log semua command yang di-exec di container —
docker exec,kubectl exec - 4Monitor file system changes — apalagi file system biner atau /etc
Gue pake Falco buat runtime threat detection di Kubernetes. Falco bisa detect:
- Shell spawning di container yang seharusnya nggak butuh shell
- Sensitive file read atau write (misal /etc/shadow)
- Unexpected network connection
- Privilege escalation attempt
Falco rules yang gue pake minimal:
- rule: Unexpected Shell In Container
desc: Detect shell spawned in container
condition: >
spawned_process and
container and
shell_procs and
not container.image.startswith("myregistry") and
not proc.name in (shell_binaries)
output: >
Shell spawned in container (user=%user.name command=%proc.cmdline container=%container.id image=%container.image.repository:%container.image.tag)
priority: WARNINGIni bikin gue tau kalo ada container yang aneh. Gue pernah dapat alert karena compromised pod nyoba install ssh client, dan Falco langsung notify lewat Slack.
CI/CD Hardening: Garis Terakhir Sebelum Production
Banyak orang lupa: CI/CD itu jembatan antara code dan production. Kalau CI/CD lo kena compromise, seluruh deployment pipeline bisa di-inject malware tanpa lo tau.
Yang pertama, limit permissions di CI runner. Jangan pakai runner yang punya akses ke seluruh production cluster. Buat dedicated service account per project, dengan permission yang cuma cukup buat deploy ke namespace yang dibutuhin. Jangan ever pakai service account yang punya cluster-admin kecuali untuk admin task yang benar-benar butuh.
Kedua, sign image dengan sigstore/cosign atau Notary. Gue pake cosign karena cuma butuh satu binary. Setiap image yang di-push ke registry harus di-sign dengan key yang gue simpen di hardware token (YubiKey). Di Kubernetes, gue pake Kyverno policy buat enforce: cuma image yang di-sign dan verified bisa di-deploy.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
rules:
- name: verify-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "myregistry.io/*"
key: "cosign.pub"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
...key material...
-----END PUBLIC KEY-----Kalau image nggak di-sign, deployment ditolak. Simple.
Ketiga, audit pipeline secara berkala. Review action yang di-pake di GitHub Actions atau GitLab CI. Gue sempat lihat repository yang pake action yang compromised karena pinned tag yang vulnerable. Lo harus pin action ke commit SHA, bukan cuma tag.
# Bad
- uses: actions/checkout@v4
# Good
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1Keempat, isolate secrets di CI. Jangan log secrets, jangan pass secrets sebagai plain text parameter. Gue pake masked variables di GitLab CI — variable yang diawali dengan MASKED_ otomatis di-hidden di log.
Kubernetes Security: RBAC dan Pod Security Standards
Kalo lo manage Kubernetes, RBAC itu wajib. Banyak cluster yang masih pake cluster-admin buat service account deployment. Itu sama aja kasih admin password ke app.
Yang pertama, buat service account per app, jangan pakai default. Lalu berikan permission cuma yang dibutuhin lewat Role dan RoleBinding. Jangan pakai ClusterRole kecuali perlu.
Contoh Role buat app yang cuma butuh baca ConfigMap:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: app-config-reader
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["myapp-config"]
verbs: ["get", "list"]Terus bind ke service account:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-config-reader-binding
namespace: production
subjects:
- kind: ServiceAccount
name: myapp-sa
namespace: production
roleRef:
kind: Role
name: app-config-reader
apiGroup: rbac.authorization.k8s.ioKalau ada attacker yang manage compromise pod, mereka cuma bisa baca ConfigMap myapp-config, bukan seluruh namespace.
Yang kedua, enable Pod Security Admission (PSA). PSA itu admission controller yang enforce standard: restricted, baseline, atau privileged. Gue pake restricted untuk production namespace — itu enforce: run as non-root, drop all capabilities, seccomp profile, read-only root filesystem. Cuma perlu beberapa baris di namespace label:
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latestTapi inget, kalau ada aplikasi legacy yang butuh root, lo harus fix aplikasinya dulu sebelum enforce restricted. Jangan turn on PSA lalu production down karena app butuh port di bawah 1024 dan running as root. Lo harus fix root cause-nya.
Disaster Recovery: Kapan Container Security Gagal
Gue ngasih contoh nyata. Tahun lalu gue manage cluster yang kena compromised. Caranya: attacker dapat RCE di container yang running as root, dengan --privileged, dan akses ke host filesystem. Dari situ mereka install cryptominer, habis CPU cluster habis.
Setelah insiden itu, gue bikin checklist post-incident:
- 1Identifikasi image yang compromised — cek CVE dan build provenance
- 2Revoke semua credential yang ada di container dan CI/CD
- 3Rotate semua secrets yang mungkin bocor
- 4Apply security context yang lebih ketat ke seluruh workload
- 5Enable audit logging dan Falco rules yang sebelumnya nggak ada
- 6Document incident dan bikin runbook buat tim
Gue sadar: container security bukan one-time setup. Perlu review berkala, terutama setelah ada CVE baru di kernel atau container runtime. Gue bikin reminder tiap bulan buat review runtime version dan image CVE scan results.
Kesimpulan
Container security itu tentang lapisan-lapisan: image yang bersih, runtime config yang ketat, network yang segmented, secrets yang di-handle dengan benar, dan detection yang jalan. Jangan cuma andal satu lapisan. Kalau lo lupa yang satu, celah tetap ada.
Gue rekomendasikan mulai dari yang paling mudah: pinned base image, non-root user, read-only root filesystem, dan drop capabilities. Itu cuma beberapa baris config, tapi bikin perbedaan besar. Lalu tingkatkan pelan-pelan: network policy, seccomp, secrets manager, image signing.
Yang penting: jangan sampai security jadi bottleneck yang nggak jalan. Lo bisa mulai dengan policy yang cuma block critical CVE dulu, lalu tingkatkan ketatnya pelan-pelan. Sepuluh workload dengan security config yang ketat lebih baik daripada ratusan workload yang nggak aman.
Ingat: container itu ephemeral. Kalau lo kena compromise, lo bisa rebuild container dari image yang bersih. Tapi kalau image lo yang compromised, atau secrets lo bocor, rebuilding cuma bikin infection yang sama kembali. Jadi fokus ke image dan runtime config — dua lapisan yang paling sering jadi pintu masuk.
Semoga panduan ini bermanfaat buat lo yang lagi belajar atau yang udah advance tapi masih butuh checklist. Cybersecurity itu journey, bukan destination. Setiap hari ada vulnerability baru, setiap bulan ada tool baru, setiap tahun ada paradigma baru. Yang penting lo mulai dan terus improve.