
Hari 18: Docker Hardening, 8 Lapis Pertahanan Container
Bikin Docker container jadi benteng dengan 8 layer security: read-only filesystem, no-new-privileges, cap_drop, resource limits. Defense in depth di praktik.
Dari Image Bersih ke Container Benteng
Kemarin kita buktikan bahwa Docker image SecureBank API punya 0 CVE dan ukurannya cuma 7.97MB. Tapi tau gak? CVE 0 saja tidak cukup. Kalau konfigurasi container-nya longgar, attacker tetap bisa masuk dan berkeliaran.
Bayangin kayak punya rumah dengan dinding tebal tapi pintu gak dikunci. Ya percuma dong.
Hari ini kita tambahkan 8 lapis pertahanan ke Docker container pakai docker-compose.yml. Setiap lapis address cara masuk yang berbeda-beda.
8 Lapis Pertahanan
Ini docker-compose.yml yang kita bikin:
services:
securebank:
build:
context: .
dockerfile: Dockerfile
image: securebank:v1
ports:
- "8080:8080"
user: "65532:65532" # Lapis 1: Nonroot user
read_only: true # Lapis 2: Filesystem read-only
tmpfs: # Lapis 3: Tmpfs /tmp
- /tmp:size=64m,mode=1777
security_opt: # Lapis 4: Anti privilege escalation
- no-new-privileges:true
cap_drop: # Lapis 5: Drop semua capabilities
- ALL
deploy:
resources:
limits:
cpus: "0.5" # Lapis 6: CPU limit
memory: 128M # Lapis 7: Memory limit
pids: 64 # Lapis 8: Batas jumlah proses
reservations:
cpus: "0.25"
memory: 64M
environment:
- JWT_SECRET=dev-secret-change-in-production
- PORT=8080
restart: unless-stopped
Mari bahas satu per satu biar jelas maksudnya apa.
Lapis 1: Jalan sebagai User Biasa (user: "65532:65532")
Angka 65532 itu ID user nonroot di distroless image. Kenapa gak pakai nama nonroot saja? Karena pakai angka lebih reliable — gak bergantung pada name resolution yang bisa aja beda di setiap image.
Intinya: container jalan sebagai user biasa, bukan root. Kalau attacker masuk, mereka cuma punya akses user biasa, bukan akses penuh ke sistem.
Lapis 2: Filesystem Read-Only (read_only: true)
Root filesystem container jadi tidak bisa ditulis. Attacker tidak bisa:
- Ubah binary aplikasi
- Tambah script backdoor
- Ganti config file
- Tulis apa-apa ke filesystem container
Tapi kan aplikasi mungkin butuh nulis sesuatu ke /tmp? Itu kenapa ada lapis berikutnya.
Lapis 3: Tmpfs untuk /tmp (tmpfs)
tmpfs:
- /tmp:size=64m,mode=1777
/tmp tetap bisa ditulis, tapi disimpan di memory (bukan disk). Maksimal 64MB. Kalau container restart, isinya hilang. Aplikasi yang butuh write sementara ke /tmp tetap bisa jalan tanpa masalah.
Tanpa ini, aplikasi yang tulis ke /tmp akan langsung crash karena filesystem-nya read-only.
Lapis 4: Anti Privilege Escalation (no-new-privileges)
security_opt:
- no-new-privileges:true
Mencegah attacker "naik kelas." Bahkan kalau mereka menemukan binary dengan permission khusus, mereka tetap gak bisa naik ke privilege yang lebih tinggi.
Ini proteksi di level kernel sistem. Sekali di-set, gak bisa dibatalkan. Pintu satu arah.
Lapis 5: Drop Semua Capabilities (cap_drop: ALL)
cap_drop:
- ALL
Container secara default punya 14 " capability" Linux yang ngasih kemampuan khusus. Dengan drop ALL, container gak punya kemampuan khusus apa pun. Cuma bisa apa yang user biasa bisa lakukan.
Lapis 6-7: Batas Resource (CPU & Memory)
limits:
cpus: "0.5" # Maks 0.5 core CPU
memory: 128M # Maks 128MB RAM
Mencegah resource exhaustion. Kalau ada memory leak atau loop tak terhingga, container gak akan bikin host-nya ikut down. Container akan throttle (CPU) atau di-kill (memory).
Lapis 8: Batas Jumlah Proses (pids: 64)
pids: 64
Container hanya bisa bikin 64 proses. Mencegah fork bomb — teknik attacker yang bikin proses bercabang tak terhingga sampai sistem crash. Dengan limit 64, fork bomb akan berhenti sendiri. Container crash, tapi host tetap aman.
Bonus: Dockerfile Juga Di-update
Dockerfile juga dikasih perbaikan:
COPY --from=builder --chown=nonroot:nonroot /securebank /securebank
COPY --from=builder --chown=nonroot:nonroot /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
--chown=nonroot:nonroot memastikan file binary dan CA certs dimiliki oleh user nonroot, bukan root. Ini best practice supaya kepemilikan file jelas dan konsisten.
Verifikasi: Semua Aktif atau Tidak?
Setelah docker compose up -d, kita cek pakai docker inspect:
$ docker inspect --format='User={{.Config.User}} ReadOnly={{.HostConfig.ReadonlyRootfs}} SecurityOpt={{.HostConfig.SecurityOpt}} CapDrop={{.HostConfig.CapDrop}} MemLimit={{.HostConfig.Memory}} NanoCpus={{.HostConfig.NanoCpus}} PidsLimit={{.HostConfig.PidsLimit}} Tmpfs={{.HostConfig.Tmpfs}}' securebank-api-securebank-1
User=65532:65532
ReadOnly=true
SecurityOpt=[no-new-privileges:true]
CapDrop=[ALL]
MemLimit=134217728
NanoCpus=500000000
PidsLimit=64
Tmpfs=map[/tmp:size=64m,mode=1777]
8 dari 8 lapis aktif. Semuanya berfungsi.
Test: Apakah Aplikasi Tetap Jalan?
Penting banget: hardening gak boleh bikin aplikasi rusak. Test-nya:
$ curl -s http://localhost:8080/health
{"status":"healthy"}
$ curl -s http://localhost:8080/balance
missing authorization header
- Health endpoint: jalan normal ✅
- Auth-protected endpoint: balas 401 tanpa JWT ✅
Aplikasi berjalan sempurna dengan semua hardening aktif. Read-only filesystem gak masalah karena SecureBank API gak nulis apa-apa ke disk — cuma nulis log ke stdout dan simpan data di memory.
Kenapa HEALTHCHECK Tidak Dimasukkan?
Distroless image itu unik — tidak punya shell, wget, atau curl. Docker HEALTHCHECK butuh command yang bisa dieksekusi. Tapi kita udah punya /health endpoint yang bisa di-hit via HTTP.
Solusinya: di Kubernetes (Fase 3 nanti), kita pakai liveness probe yang nge-hit /health via HTTP GET. Lebih powerful dan gak butuh executable di dalam container.
Sebelum vs Sesudah
| Setting | Sebelum (Day 16) | Sesudah (Day 18) |
|---|---|---|
| User | nonroot di Dockerfile | nonroot di Dockerfile + compose |
| Filesystem | Bisa ditulis | Read-only + tmpfs /tmp |
| Privilege | Default | no-new-privileges + cap_drop ALL |
| Resource | Unlimited | 0.5 CPU, 128MB RAM, 64 proses |
| File Ownership | Root punya binary | nonroot punya (--chown) |
Filosofi: Defense in Depth
Gak ada satu lapis yang "magic bullet." Tapi 8 lapis bersama-sama bikin container jadi benteng:
- Attacker masuk → nonroot (Lapis 1)
- Mau tulis backdoor → read-only (Lapis 2)
- Mau naik privilege → no-new-privileges (Lapis 4)
- Mau pakai capability kernel → cap_drop ALL (Lapis 5)
- Mau fork bomb → pids limit 64 (Lapis 8)
- Mau OOM host → memory limit 128MB (Lapis 7)
- Mau rakit CPU → cpu limit 0.5 (Lapis 6)
Setiap attack vector ada countermeasure-nya.
Lesson Learned
1. Read-only filesystem butuh tmpfs. Kalau aplikasi butuh nulis ke /tmp tapi filesystem-nya read-only, aplikasi akan crash. Selalu sediakan tmpfs dan test setelah enable read-only.
2. cap_drop ALL aman untuk aplikasi yang gak butuh akses khusus. SecureBank API cuma denger di port 8080 (di atas 1024) dan gak butuh raw socket. Kalau aplikasi butuh port di bawah 1024, harus add capability NET_BIND_SERVICE secara spesifik.
3. no-new-privileges itu proteksi kernel, bukan fitur Docker. Sekali flag ini di-set, gak bisa dibatalkan. Ini kayak gembok satu arah — sekali dikunci, gak bisa dibuka lagi dari dalam container.
4. UID 65532 itu konvensi distroless. Bukan 1000 atau 1001. Selalu cek UID user di base image sebelum set user: di docker-compose.yml. Kalau salah UID, container bisa gagal start.
5. HEALTHCHECK di-skip di Dockerfile bukan berarti diabaikan. Distroless gak punya executable untuk healthcheck, tapi /health endpoint udah ada. Nanti di Kubernetes pakai liveness probe via HTTP GET — lebih powerful.
Kesimpulan
Hari ini kita tambahkan 8 lapis security hardening ke docker-compose.yml plus COPY --chown di Dockerfile. Container SecureBank API sekarang jalan dengan filesystem read-only, gak bisa privilege escalation, semua capabilities di-drop, dan ada batas resource yang ketat.
Filonofi hari ini: defense in depth. Bukan satu tembok tebal, tapi 8 lapis pertahanan yang saling melengkapi. Kalau satu lapis kena tembus, masih ada 7 lapis lain yang nahan.
Besok: Hari 19 — Image Signing (Cosign) — sign Docker image dengan cryptographic key biar bisa diverifikasi integritasnya.
Diskusi & Komentar
Hari 17: Distroless 8MB vs Alpine 352MB, Sama-Sama 0 CVE
Next ArticleHari 19: Sign Docker Image Pakai Cosign, Verifikasi Integritas
Artikel Terkait
Hari 5: Trivy SCA Scan Nemukan 4 CVE di Golang API
Hari kelima 60 hari DevSecOps! Scan dependensi Go pakai Trivy dan nemukan 4 CVE termasuk 1 CRITICAL — termasuk library deprecated jwt-go.
Hari 8: Semgrep SAST Scan Temukan Kode Tidak Aman
Hari kedelapan 60 hari DevSecOps! Install Semgrep, buat kode insecure (MD5), dan scan ketemu 2 finding — MD5 weak hash dan HTTP server tanpa TLS.
Hari 10: MD5 ke Bcrypt, Pipeline Hijau Lagi
Hari kesepuluh 60 hari DevSecOps! Fix SAST findings — ganti MD5 ke bcrypt, hapus custom rule HTTP TLS, dan pipeline CI kembali hijau setelah 4 job semua pass.