Hari 18: Docker Hardening, 8 Lapis Pertahanan Container
6 min read

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.

devsecops
docker
container-security
hardening
60-days-challenge
Share

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.

Repo: github.com/stayrelevantid/chalange-devsecops

Enjoyed this article? Share it!

Share

Diskusi & Komentar

Artikel Terkait