Hari 16: Docker Image 8MB, Distroless Bikin Aman
5 min read

Hari 16: Docker Image 8MB, Distroless Bikin Aman

Fase 2 dimulai! Bikin Docker image SecureBank API cuma 8MB pakai multi-stage build dan distroless. Build di alpine, run tanpa OS — attack surface minimal.

devsecops
docker
distroless
multi-stage-build
container-security
60-days-challenge
Share

Fase 2 Dimulai: Dari Kode ke Container

Fase 1 kemarin fokusnya ke kode — pipeline CI/CD, SAST, SCA, secret scanning, input validation, JWT auth, threat modeling. Semua di level source code.

Fase 2 sekarang fokusnya ke infrastruktur — container, Terraform, DAST. Dan langkah pertama: bungkus aplikasi ke dalam Docker image yang sekecil dan seaman mungkin.

Kenapa Multi-stage Build?

Bayangkan dua skenario:

Skenario 1: Single-stage build

FROM golang:1.26
COPY . .
RUN go build -o /securebank ./cmd/api
ENTRYPOINT ["/securebank"]

Image size: ~800MB. Kenapa? Karena image bawa seluruh Go toolchain, Alpine package manager, git, dan ratusan library yang gak dibutuhin di runtime. Attacker yang masuk ke container ini bisa apk add, curl, wget, dan sh — semua tersedia.

Skenario 2: Multi-stage build

FROM golang:1.26-alpine AS builder  # ~300MB (buang setelah build)
RUN go build -ldflags="-w -s" -o /securebank ./cmd/api

FROM gcr.io/distroless/static-debian12:nonroot  # ~2MB
COPY --from=builder /securebank /securebank
ENTRYPOINT ["/securebank"]

Image size: 7.97MB. Hanya binary + CA certs. Tidak ada shell, tidak ada package manager, tidak ada utilitas apapun. Attacker yang masuk gak bisa apa-apa karena... memang gak ada apa-apanya.

Apa yang Dibuat Hari Ini

Dockerfile Multi-stage

Stage 1 (builder):

  • Base image: golang:1.26-alpine (~300MB)
  • Install git dan ca-certificates
  • Download Go modules (go mod download)
  • Build binary dengan CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s"

Stage 2 (runtime):

  • Base image: gcr.io/distroless/static-debian12:nonroot (~2MB)
  • Copy binary dari builder stage
  • Copy CA certificates (buat HTTPS request ke external API kalau perlu)
  • USER nonroot:nonroot — container jalan sebagai user biasa, bukan root
  • EXPOSE 8080
  • ENTRYPOINT ["/securebank"]

.dockerignore

Datangnya kecil tapi penting — .dockerignore mencegah file gak perlu masuk ke Docker build context:

.git
.github
docs
progress
security
*.md
*.test
*.out
coverage.out
.env
.env.*

Tanpa ini, seluruh repo (termasuk .git, report JSON, dll) bakal di-copy ke builder stage dan memperlambat build.

Hasil: 7.97MB

$ docker images securebank:v1
REPOSITORY    TAG    IMAGE ID       SIZE
securebank    v1     4f8e6cb6a41e   7.97MB

Target < 15MB, hasil 7.97MB. Distroless + stripped binary = ultra-compact.

Container Test

$ docker run -d -p 8080:8080 -e JWT_SECRET=test-secret securebank:v1
$ curl http://localhost:8080/health
{"status":"healthy"}

Aplikasi berjalan normal di container. /health endpoint merespons karena memang dirancang public (tanpa auth).

Beberapa Detail yang Penting

CGO_ENABLED=0 itu wajib untuk distroless

Distroless static-debian12 gak punya glibc. Kalau binary di-link secara dinamis ke glibc (default behaviour Go kalau CGO aktif), binary bakal crash di runtime. CGO_ENABLED=0 bikin binary statically-linked — semua library sudah di-include di dalam binary.

-ldflags="-w -s" bikin binary lebih kecil

  • -w = strip DWARF debug info (gak perlu di production)
  • -s = strip symbol table (nama fungsi, variabel, dll)

Hasilnya binary Go yang tadinya ~12MB jadi ~7MB. Bonus: attacker gak bisa objdump atau strings untuk melihat detail internal binary.

Distroless gak punya shell

Ini feature, bukan bug. Tidak ada sh, bash, apk, apt, curl, wget, atau utilitas apapun. Keuntungannya:

  1. Attack surface minimal — gak ada yang bisa di-exploit
  2. Image size kecil — gak ada layer yang gak perlu
  3. Gak bisa di-debug langsung — kalau mau debug, harus pakai docker cp atau ephemeral debug container

Kalau butuh debug, bisa pakai:

kubectl debug -it securebank-pod --image=busybox --target=securebank

USER nonroot:nonroot

Container jalan sebagai user nonroot (UID 65534), bukan root. Ini berarti kalau attacker berhasil masuk, mereka gak punya akses root di container. Ini gak sempurna (container escape masih mungkin), tapi menambahkan satu lapis pertahanan lagi.

Lesson Learned

1. Multi-stage build itu fundamental untuk container security. Build stage bawa toolchain yang besar (300MB+), tapi hanya binary final yang masuk ke runtime stage. Attack surface drastis berkurang — dari OS lengkap ke binary saja.

2. Image size kecil = attack surface kecil. 800MB single-stage vs 8MB multi-stage. Lebih sedikit code di production = lebih sedikit celah. Ini prinsip yang sama kayak "minimal installation" di server biasa.

3. Distroless itu security by default. Gak ada shell = gak bisa di-exploit dengan shell-based attack. Attacker masuk ke container dan... gak bisa apa-apa. Tapi ini juga artinya debugging lebih susah — harus pakai debug container terpisah.

4. CGO_ENABLED=0 bukan cuma opsi, itu kebutuhan. Tanpa flag ini, binary bond to glibc dan gak bakal jalan di distroless. Ini mistake yang sering terjadi pertama kali build Go image untuk distroless.

5. .dockerignore itu seperti .gitignore tapi untuk container. File yang gak perlu (.git, docs, test files) gak usah masuk ke build context. Ini bikin build lebih cepat dan image lebih kecil.

Kesimpulan

Hari ini SecureBank API resmi jadi container. Dari binary Go yang jalan di laptop ke Docker image 7.97MB yang jalan di distroless. Build di alpine (300MB+), run di distroless (2MB base). Container jalan sebagai nonroot, gak ada shell, gak ada utilitas.

Ini baru permulaan Fase 2. Besok: scan image ini dengan Trivy dan bandingkan hasilnya dengan image "naif" yang pakai alpine base. Spoiler: perbedaannya bakal signifikan.

Repo: github.com/stayrelevantid/chalange-devsecops

Enjoyed this article? Share it!

Share

Diskusi & Komentar

Artikel Terkait