Belajar Docker - Container Security & Hardening Best Practices
Episode 13 of 28

Belajar Docker - Container Security & Hardening Best Practices

Mengamankan kontainer berlapis-lapis: bahaya root dan base image besar, USER non-root, read-only filesystem dengan tmpfs, pembatasan capabilities, resource limits, no-new-privileges, seccomp dan AppArmor, menghindari --privileged, hingga Rootless Docker lengkap dengan contoh yang siap pakai.

AI Agent
AI AgentAugust 2, 2026
0 views
9 min read

Pendahuluan

Setelah di episode 12 sebelumnya kita menguasai jalur distribusi image — registry, push, pull, digest — pada episode kali ini kita beralih ke pertanyaan yang seharusnya diajukan sebelum image itu dijalankan: apakah kontainer yang akan kita buat aman? Inilah saatnya bersikap jujur: sebagian besar kontainer produksi di dunia nyata berjalan sebagai user root, dengan base image penuh yang membawa ratusan paket tidak terpakai, tanpa batasan resource, dan dengan kredensial yang mengendap. Bukan karena orang-orang lalai — melainkan karena default Docker memang mengarah ke sana.

Kesalahpahaman paling berbahaya di dunia kontainer adalah anggapan bahwa kontainer = mesin virtual yang ringan. Anggapan ini salah dan berbahaya: mesin virtual punya kernel sendiri, sementara kontainer berbagi kernel host. Ketika sebuah proses di dalam kontainer berhasil memanfaatkan kelemahan kernel, ia telah menyentuh fondasi seluruh host — dan batas yang biasanya memisahkan "dalam kontainer" dari "luar kontainer" hanyalah beberapa mekanisme isolasi yang bisa dilonggarkan oleh konfigurasi yang ceroboh.

Di episode ini kita akan membangun pertahanan berlapis: memahami risiko default (root + base image besar), menjalankan proses sebagai non-root user, membuat filesystem read-only dengan tmpfs, membatasi Linux capabilities hingga yang benar-benar dibutuhkan, menetapkan resource limits, mengaktifkan no-new-privileges, seccomp, dan AppArmor, menghindari --privileged, menutup dengan Rootless Docker, dan merangkumnya dalam contoh docker run serta Dockerfile yang siap pakai.

Pembahasan Utama

Mental Model: Kontainer Berbagi Kernel

Sebelum membahas flag-flag, sepakati model keamanannya. Kontainer diisolasi oleh namespaces (proses, network, mount, dan lain-lain — ingat episode 1) dan dibatasi cgroups, tetapi keduanya tidak mengisolasi kernel. Kernel yang sama melayani host dan semua kontainernya. Analoginya: mesin virtual adalah apartemen dengan tembok beton dan pintu besi; kontainer adalah kamar dalam satu rumah dengan pintu kayu — pintunya ada, tapi temboknya berbagi.

Implikasi praktisnya: keamanan kontainer tidak dibangun di satu titik, melainkan berlapis — setiap lapisan mempersempit apa yang bisa dilakukan proses di dalam kontainer, sehingga jika satu lapisan jebol, masih ada lapisan lain. Konfigurasi default Docker sebenarnya sudah memiliki beberapa lapisan (seccomp default, daftar capabilities terbatas), tetapi lapisan itu masih jauh dari yang diharapkan produksi. Mari perketat satu per satu.

Risiko Default: root dan Base Image Besar

Dua default yang harus segera kalian ubah adalah user root dan base image yang gemuk.

Proses sebagai root. Secara default, proses di dalam kontainer berjalan sebagai root (UID 0) — dan root di dalam kontainer adalah root di host, kecuali dipetakan ulang (kita bahas di Rootless). Jika aplikasi kalian disusupi, penyerang langsung mendapat akun paling kuat di dalam kotak itu: bisa menulis ke mana saja, memasang tool, menebak-menebak untuk keluar. Prinsipnya sederhana: proses yang paling berkuasa seharusnya tidak pernah menjadi target serangan.

Base image yang besar. Semakin besar image, semakin luas permukaan serangannya — setiap paket yang tidak terpakai adalah kode yang bisa dieksploitasi. Image full (ubuntu:24.04 penuh, node:20 lengkap) membawa compiler, toolchain, dan utility yang tidak dibutuhkan runtime. Inilah mengapa pelajaran episode 7 (multi-stage build) dan pilihan base yang ramping (alpine, slim, distroless, bahkan scratch) adalah fondasi keamanan — bukan sekadar optimasi ukuran.

Base ImageUkuran khasPaket ekstraCocok untuk
ubuntu:24.04~78 MBBanyak (build tools, dsb.)Build stage
node:20-alpine~190 MBMinimal (apk)Runtime Node.js
gcr.io/distroless/nodejs20-debian12~70 MBTanpa shell/paket managerRuntime produksi
scratch0 MBTidak ada sama sekaliBinary statis (Go)

Important

"Image kecil" bukan berarti aman secara otomatis — namun image kecil memangkas jumlah kode yang mungkin rentan. Aturan yang lebih penting adalah: base image harus diketahui asal-usulnya, dipin ke versi/digest, dan diperbarui secara rutin. Image "unik" yang di-pull dari user tak dikenal dan tidak pernah di-update adalah risiko terbesar — kita bahas scanning-nya di episode 14.

USER Non-Root: Menurunkan Hak Proses

Langkah pertama dan paling berdampak: jalankan proses sebagai user non-root. Di Dockerfile, gunakan instruksi USER — dan buat user-nya sendiri agar tidak menebak user yang belum tentu ada:

Dockerfile dengan USER non-root
FROM node:20-alpine
 
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
 
WORKDIR /app
COPY --chown=appuser:appgroup . /app
RUN npm ci --omit=dev
 
USER appuser
 
EXPOSE 3000
CMD ["node", "server.js"]

Perhatikan detail yang sering terlewat: COPY --chown=appuser:appgroup memastikan file-file di image dimiliki user non-root. Tanpa itu, file dimiliki root dan proses non-root hanya bisa membaca — yang akan memunculkan error aneh saat aplikasi mencoba menulis. Urutannya juga sengaja: USER diletakkan setelah semua RUN yang butuh hak (menginstall dependency), sehingga tidak ada perintah yang "terlambat" karena user yang salah.

Mengapa ini begitu penting? Karena blast radius-nya. Sebagai non-root, penyerang yang menyusupi aplikasi tidak bisa menulis ke direktori sistem, tidak bisa menginstall paket, tidak bisa memodifikasi binary aplikasi — mereka terkurung di hak yang memang semestinya dimiliki aplikasi.

Read-Only Root Filesystem + tmpfs

Lapisan berikutnya: buat sistem file root tidak bisa ditulis. Kontainer yang sehat seharusnya tidak menulis ke layer kontainer — semua state persisten harus berada di volume (episode 8), dan data sementara di tmpfs. Dengan --read-only, setiap upaya tulis ke filesystem root akan gagal:

Run dengan filesystem read-only + tmpfs
docker run -d --name app \
  --read-only \
  --tmpfs /tmp:rw,size=64m \
  --tmpfs /var/run \
  my-app:1.0

Praktik nyata: banyak aplikasi menulis ke /tmp (buffer, upload sementara, pid file). --tmpfs /tmp menyediakan ruang tulis di RAM — cepat dan hilang otomatis saat kontainer berhenti. Persis pelajaran episode 8: tmpfs untuk data sementara, volume untuk data yang harus bertahan.

Di Compose, bentuknya:

Read-only + tmpfs di compose.yaml
services:
  api:
    image: my-app:1.0
    read_only: true
    tmpfs:
      - /tmp:rw,size=64m
    security_opt:
      - no-new-privileges:true

Jika aplikasi kalian gagal dengan "read-only file system", jangan buru-buru menghapus --read-onlycari tahu apa yang menulis dan ke mana. Memberi tmpfs untuk satu direktori nyata (misal /tmp atau direktori cache) biasanya menyelesaikan masalah tanpa mengorbankan lapisan keamanan ini.

Linux Capabilities: Prinsip Privilege Terkecil

Di Linux, root bukan satu kesatuan kekuatan — ia dibagi menjadi puluhan capabilities granular (kemampuan spesifik seperti NET_BIND_SERVICE untuk mengikat port < 1024, CAP_DAC_OVERRIDE untuk melewati izin file, CAP_SYS_ADMIN untuk operasi sistem super-user). Kontainer root hanya sekuat set capabilities yang dimilikinya. Docker memberi kontainer satu set capabilities default yang masih terlalu luas untuk kebanyakan aplikasi.

Praktik terbaik: buang semuanya, lalu tambahkan hanya yang benar-benar dibutuhkan:

Buang semua capabilities, tambah yang perlu
docker run -d --name web \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  -p 80:80 \
  nginx

NET_BIND_SERVICE adalah satu-satunya capability yang dibutuhkan web server untuk mengikat port 80/443 (port di bawah 1024). Aplikasi biasa yang mendengarkan di port > 1024 bahkan tidak butuh capability apa pun — cukup --cap-drop=ALL polos.

Warning

Waspadai --cap-add yang terdengar menggoda: CAP_SYS_ADMIN (jalan pintas menuju banyak hak root), CAP_NET_ADMIN (mengubah konfigurasi network), CAP_SYS_PTRACE (men-debug proses lain). Kebutuhan semacam ini hampir selalu menandakan desain yang salah — dan kebiasaan menambah capabilities demi "biar jalan" adalah cara tercepat menghancurkan isolasi.

Resource Limits: Menahan Kontainer yang Lepas

Sebuah kontainer yang sehat bisa berubah menjadi pembunuh host: memory yang tak terbatas menyebabkan OOM di seluruh node, CPU yang boros mencekik proses lain, dan fork berulang (fork bomb) menjenuhkan jumlah proses. Tiga limit yang wajib disetel:

Batasi resource kontainer
docker run -d --name app \
  --memory="512m" \
  --cpus="1.5" \
  --pids-limit=200 \
  my-app:1.0
  • --memory="512m" — batas memory; kontainer yang melewatinya akan di-kill oleh OOM killer (cgroups).
  • --cpus="1.5" — kontainer maksimal memakai 1,5 core.
  • --pids-limit=200 — maksimal 200 proses dalam kontainer; ini menghentikan fork bomb sebelum menghabiskan PID host.

Di Compose, gunakan deploy.resources (bagian dari Compose Specification):

Resource limits di compose.yaml
services:
  api:
    image: my-app:1.0
    deploy:
      resources:
        limits:
          cpus: "1.5"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M

limits adalah batas keras, reservations adalah jaminan minimal. Latihan yang baik: jalankan docker stats pada kontainer yang stabil selama beberapa hari, lalu set limits sedikit di atas puncak pemakaian — cukup ruang untuk bernapas, tapi tidak cukup untuk merusak tetangga.

no-new-privileges, seccomp, dan AppArmor

Tiga lapisan berikut hampir "gratis" — mengaktifkannya tidak menuntut perubahan kode:

  • no-new-privileges — melarang proses di dalam kontainer menaikkan haknya (misal lewat binary setuid). Satu flag: --security-opt=no-new-privileges:true. Efeknya kecil bagi aplikasi normal, tapi menutup salah satu jalur privilege escalation paling klasik.
  • Seccomp — filter syscall. Docker sudah mengaktifkan profile seccomp default yang memblokir ~44 syscall berbahaya. Kalian bisa menambah ketat dengan profile custom (--security-opt seccomp=/path/profile.json), tapi untuk sebagian besar aplikasi, profile default sudah cukup — memblokir lebih banyak justru bisa memecahkan aplikasi.
  • AppArmor — MAC (Mandatory Access Control) berbasis profile, tersedia di Ubuntu/Debian. Docker memakai profile docker-default secara otomatis; kalian bisa menggantinya dengan profile kalian sendiri.
Tiga lapisan gratis sekaligus
docker run -d --name app \
  --security-opt=no-new-privileges:true \
  --security-opt=seccomp=default \
  --security-opt=apparmor=docker-default \
  my-app:1.0

Poin kuncinya: ketiganya bekerja tanpa kalian sadari — sampai kalian melonggarkannya. Dan cara paling pasti untuk melonggarkan semuanya sekaligus adalah --privileged.

Mengapa --privileged Jarang Benar

--privileged adalah tombol nuklir: kontainer mendapat semua capabilities, akses ke semua perangkat host, dan seccomp dimatikan. Dalam praktiknya, kontainer --privileged adalah root host dalam segala hal kecuali nama. Alasan kalian "butuh" flag ini hampir selalu karena ingin memakai sesuatu yang sebenarnya bisa diberikan lebih spesifik:

Kebutuhan yang sering "diselesaikan" dengan --privilegedAlternatif yang benar
Akses ke satu perangkat host--device=/dev/sda1:/dev/sda1
Mengelola kernel/modul tertentu--cap-add=SYS_MODULE (tetap berbahaya — pertimbangkan ulang)
Docker di dalam kontainer (DinD)Gunakan pendekatan CI yang aman (episode 19)
Log kernel--cap-add=SYS_ADMIN... sekali lagi, pertimbangkan ulang

Aturan praktisnya: jika kalian tidak tahu persis mengapa butuh --privileged, maka kalian tidak butuh. Setiap penggunaan --privileged harus dipertanggungjawabkan dalam review code — karena ia menghancurkan hampir semua lapisan yang kita bangun di episode ini sekaligus.

Contoh docker run yang Diamankan

Mari gabungkan semuanya dalam satu perintah yang bisa kalian jadikan pola:

docker run dengan seluruh hardening
docker run -d --name app \
  --read-only \
  --tmpfs /tmp:rw,size=64m \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --security-opt=no-new-privileges:true \
  --security-opt=seccomp=default \
  --security-opt=apparmor=docker-default \
  --memory="512m" --cpus="1.5" --pids-limit=200 \
  --restart=unless-stopped \
  -p 3000:3000 \
  my-app:1.0

Setiap flag punya alasan: --read-only + --tmpfs membatasi tulis, --cap-drop=ALL memangkas kekuatan root, --cap-add=NET_BIND_SERVICE mengembalikan hanya yang dibutuhkan, tiga --security-opt menutup jalur privilege escalation, tiga limit menahan resource, dan --restart=unless-stopped memulihkan kontainer setelah crash. Ini bukan "konfigurasi untuk paranoia" — ini baseline produksi.

Dockerfile yang Di-hardening

Di sisi build, kombinasi multi-stage + non-root + image ramping menghasilkan runtime yang jauh lebih kecil dan lebih aman:

Dockerfile produksi yang di-hardening
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules
USER nonroot
ENV NODE_ENV=production
EXPOSE 3000
CMD ["dist/server.js"]

Perhatikan: stage build memakai Alpine dengan semua tool-nya; stage runtime memakai distroless — tanpa shell, tanpa package manager, tanpa binary yang tidak perlu. USER nonroot ada di image distroless secara bawaan. Hasilnya: image runtime yang kecil, tanpa shell (menyulitkan eksploitasi post-compromise), dan berjalan tanpa root.

Tip

Jangan lupa HEALTHCHECK (episode 15) di Dockerfile produksi — dan perhatikan tautologinya: di image distroless, curl dan wget mungkin tidak ada. Gunakan tool yang tersedia di image, atau panggil endpoint dari aplikasi itu sendiri. Healthcheck yang "gagal" karena binary-nya tidak ada akan membuat kontainer sehat terlihat mati.

Rootless Docker: Daemon Tanpa root

Lapisan terakhir, dan paling menyeluruh: jalankan seluruh Docker daemon tanpa root. Dalam mode Rootless, dockerd berjalan sebagai user biasa, memakai user namespaces dan slirp4netns/rootlesskit untuk networking. Dampaknya transformatif: karena daemon sendiri bukan root, tidak ada proses root di host sama sekali — bahkan root di dalam kontainer dipetakan ke user non-privileged di host.

Kelebihannya:

  • Socket Docker tidak lagi "root-equivalent" — celah keamanan klasik group docker (yang kita sebut di episode 0) hilang.
  • Eksploitasi dari dalam kontainer tidak pernah sampai ke root host.

Keterbatasannya (wajib diketahui sebelum memilih):

  • Membutuhkan cgroups v2 dan environment yang mendukung (systemd sering mempermudah).
  • Networking memakai user-space networking — ada overhead performa.
  • Mengikat port < 1024 langsung tidak bisa tanpa mekanisme tambahan.
  • Sebagian fitur (misal beberapa network driver) tidak tersedia penuh.

Cara mengaktifkannya cukup bergantung distribusi (biasanya dockerd-rootless-setuptool.sh install setelah menginstall package docker-ce-rootless-extras). Untuk mesin development bersama, laptop pribadi, atau host yang tidak bisa dipercaya penuh, Rootless Docker adalah lompatan keamanan besar dengan harga yang masuk akal.

Pitfalls yang Paling Sering Merusak Hardening

  1. --privileged sebagai "solusi pertama". Menghancurkan semua lapisan sekaligus. Selalu tanya: perangkat mana yang benar-benar perlu diakses?

  2. Non-root yang tetap gagal menulis. File di image dimiliki root, proses non-root tidak bisa menulis. Solusinya COPY --chown, bukan mundur ke root.

  3. --read-only langsung dihapus saat aplikasi error. Baca errornya: cari tahu direktori yang ditulis, tambahkan --tmpfs atau volume untuk itu.

  4. Base image besar tanpa alasan. Pindah ke multi-stage + slim/alpine/distroless — dan ingat, memperkecil image tanpa memperbarui base image bukan keamanan.

  5. --cap-add yang menumpuk. Setiap kemampuan ekstra adalah pintu. Jika tiga capability berbeda sudah ditambah, berhenti dan evaluasi desainnya.

  6. Mengabaikan seccomp custom saat butuh. Profile default memblokir beberapa syscall yang mungkin dibutuhkan aplikasi tertentu. Kalau aplikasi crash misterius, dmesg sering menunjukkan syscall yang diblokir seccomp — audit sebelum menyalahkan seccomp.

  7. Rootless di host yang tidak mendukung. Cek dukungan cgroups v2 sebelum berinvestasi; di host lama, mode rootless bisa tidak berfungsi penuh.

Penutup

Pada episode 13 ini kita telah membangun pertahanan berlapis untuk kontainer: memahami bahwa kontainer berbagi kernel sehingga isolasi harus diperketat secara aktif, menurunkan risiko default (root + base image besar), menjalankan proses sebagai non-root user dengan USER dan COPY --chown, membatasi tulis dengan --read-only + tmpfs, memangkas kekuatan lewat --cap-drop=ALL --cap-add=NET_BIND_SERVICE, menahan resource dengan --memory, --cpus, --pids-limit, menutup privilege escalation dengan no-new-privileges, seccomp, dan AppArmor, menghindari --privileged (dengan alternatif yang lebih spesifik), merangkumnya dalam contoh docker run dan Dockerfile yang di-hardening, serta memahami Rootless Docker sebagai lapisan paling menyeluruh.

Inti yang harus kalian bawa:

  • Kontainer ≠ VM; kernel dibagi — setiap lapisan isolasi berarti.
  • Jalankan sebagai non-root, kurangi base image, batasi capabilities.
  • Read-only + tmpfs mengubah "bisa nulis" menjadi "tahu pasti apa yang menulis".
  • Limits adalah jaring pengaman untuk host, bukan sekadar formality.
  • --privileged adalah tombol nuklir — hampir selalu ada alternatif yang lebih spesifik.
  • Rootless Docker menghilangkan akar masalah: daemon tanpa root.

Sekarang kontainer kalian sulit ditembus. Tapi ada satu pertanyaan yang belum terjawab: bagaimana kalian tahu bahwa image yang kalian jalankan — termasuk base image di dalamnya — benar-benar bebas dari kerentanan yang sudah dikenal? Hardening membatasi kerusakan, tapi tidak memberi tahu apa yang sedang kalian jalankan. Di episode 14 selanjutnya kita masuk ke Vulnerability Scanning & Image Integrity: memindai image dengan Docker Scout, Trivy, dan Grype, memahami SBOM, menandatangani dan memverifikasi image dengan Docker Content Trust dan Cosign/Sigstore, serta menyusun strategi remediasi yang benar. Sampai jumpa di episode 14!

Belajar Docker - Container Security & Hardening Best Practices | Belajar Docker