Container menjual isolasi, tetapi isolasinya lebih tipis dari kedengarannya: semua container di satu host berbagi satu kernel, dan secara bawaan container berjalan sebagai root. Dua fakta itu membuat container bukan tembok kokoh melainkan pagar yang harus kita tinggikan lapis demi lapis. Chapter ini menyusun pertahanan berlapis itu dari bawah ke atas: Dockerfile yang aman, audit otomatis, penanganan secret yang benar, hingga pembatasan runtime. Docker tidak terpasang di lingkungan latihan, jadi seluruh latihan berjalan di atas fixture Dockerfile dan JSON yang dibedah dengan diff, grep, dan jq.
Risiko Bawaan Container #
Empat lubang paling sering ditemukan pada setup container yang dibiarkan bawaan:
| Risiko | Kenapa terjadi | Mitigasi |
|---|---|---|
| container berjalan sebagai root | Dockerfile tanpa instruksi USER | USER non-root, atau rootless mode |
| supply chain tak terkendali | FROM image:latest dari sumber asing | pin versi dan digest, image resmi |
| secret terkubur di image | ENV PASSWORD=... di Dockerfile | file/secret runtime, bukan layer |
| container tanpa batas | memori dan CPU bebas dipakai | --memory, --cpus |
Akar soalnya satu: kelompok risiko di kiri terasa tidak berbahaya karena container “sudah terisolasi”. Kenyataannya, isolasi container berbagi kernel dengan host — kerentanan container escape yang menemukan root di dalam container berusaha keras memperlebar cakarannya ke host. Setiap lapis mitigasi memperkecil apa yang bisa diraih saat lapis pertama jebol.
Dockerfile Aman vs yang Berisiko #
Dua fixture di bundle materi — Dockerfile.riskan dan Dockerfile.aman — membangun aplikasi Node.js yang sama; bedanya hanya disiplin penulisnya. Kita tampilkan keduanya lalu bedah dengan diff:
echo '== Dockerfile.riskan =='
cat Dockerfile.riskan
echo '== Dockerfile.aman =='
cat Dockerfile.aman
echo '== perbedaannya =='
diff -u -L Dockerfile.riskan -L Dockerfile.aman Dockerfile.riskan Dockerfile.aman || trueOutput yang Dihasilkan:
== Dockerfile.riskan ==
FROM node:latest
ENV DB_PASSWORD=CONTOH_PASSWORD_KUAT
COPY . /app
RUN curl -s https://contoh.example.com/install.sh | sh
CMD ["node", "/app/server.js"]
== Dockerfile.aman ==
FROM node:22.11-alpine
WORKDIR /app
COPY package.json .
RUN npm ci --omit=dev && npm cache clean --force
COPY . .
USER node
CMD ["node", "server.js"]
== perbedaannya ==
--- Dockerfile.riskan
+++ Dockerfile.aman
@@ -1,5 +1,7 @@
-FROM node:latest
-ENV DB_PASSWORD=CONTOH_PASSWORD_KUAT
-COPY . /app
-RUN curl -s https://contoh.example.com/install.sh | sh
-CMD ["node", "/app/server.js"]
+FROM node:22.11-alpine
+WORKDIR /app
+COPY package.json .
+RUN npm ci --omit=dev && npm cache clean --force
+COPY . .
+USER node
+CMD ["node", "server.js"]Bacaan diff-nya, potongan demi potongan:
FROM node:latestmenjadiFROM node:22.11-alpine: versi dipin ke angka konkret dan varian alpine yang ramping;latestberubah diam-diam — build hari ini dan bulan depan bisa jadi dua aplikasi berbeda.- Baris
ENV DB_PASSWORDhilang: kredensial tidak pernah menyentuh image — environment image terlihat oleh siapa pun yang bisa menarik image itu, selamanya. COPY package.jsonlaluRUN npm ci: layer dependensi terpisah dari source code —COPY . .di bawahnya tidak memicu instalasi ulang selamapackage.jsontidak berubah; cache layer bekerja, image lebih kecil.USER node: proses berjalan sebagai usernodebawaan image, bukan root — satu baris ini menutup risiko terbesar tabel di atas.CMD ["node", "server.js"]: bentuk exec form denganWORKDIR /app, tanpa path absolut yang rapuh.
Audit Dockerfile dengan Skrip Mini #
Empat pemeriksaan yang menutup tabel risiko di atas bisa ditulis sebagai skrip grep sederhana — versi mini dari linter yang sesungguhnya:
audit() {
file=$1
echo "== audit $file =="
if grep -q 'FROM .*:latest' "$file"; then
echo " [X] FROM memakai tag :latest"
else
echo " [ok] image dasar dipin"
fi
if grep -q '^USER ' "$file"; then
echo " [ok] ada USER non-root"
else
echo " [X] tanpa USER: berjalan sebagai root"
fi
if grep -Eq '(curl|wget)[^|]*\|[[:space:]]*(ba)?sh' "$file"; then
echo " [X] pola unduh-lalu-eksekusi"
else
echo " [ok] tidak ada pola unduh-lalu-eksekusi"
fi
if grep -Eq '^ENV .*(PASSWORD|SECRET|TOKEN)' "$file"; then
echo " [X] secret tertulis di ENV"
else
echo " [ok] tidak ada secret di ENV"
fi
}
audit Dockerfile.riskan
audit Dockerfile.amanOutput yang Dihasilkan:
== audit Dockerfile.riskan ==
[X] FROM memakai tag :latest
[X] tanpa USER: berjalan sebagai root
[X] pola unduh-lalu-eksekusi
[X] secret tertulis di ENV
== audit Dockerfile.aman ==
[ok] image dasar dipin
[ok] ada USER non-root
[ok] tidak ada pola unduh-lalu-eksekusi
[ok] tidak ada secret di ENVCara kerjanya: fungsi audit menerima satu berkas, lalu empat pasang if grep menguji pola berisiko dan melaporkan hasilnya — :latest di baris FROM, absennya USER, pola curl ... | sh yang mengeksekusi skrip unduhan, dan kata sandi di ENV. Skrip 25 baris ini menutup keempat lubang tabel pembuka; untuk pemeriksaan mendalam, hadolint me-lint Dockerfile dan trivy memindai image jadi — keduanya standar di pipeline CI sebelum image diberi izin masuk registry.
Secret Bukan Environment Variable #
Menaruh password di docker run -e DB_PASSWORD=... terasa praktis, tetapi seluruh nilainya terbaca oleh siapa pun yang bisa menjalankan docker inspect pada container itu. Fixture inspect-web.json memperlihatkan betapa terbukanya:
echo '== docker inspect web-toko (potongan) =='
jq -r '.[0].Config.Env[]' inspect-web.json
echo '== user prosesnya =='
jq -r '.[0].Config.User | if . == "" then "(kosong: berjalan sebagai root)" else . end' inspect-web.jsonOutput yang Dihasilkan:
== docker inspect web-toko (potongan) ==
DATABASE_URL=postgres://budi:CONTOH_PASSWORD_KUAT@db:5432/toko
PATH=/usr/local/bin:/usr/bin:/bin
== user prosesnya ==
(kosong: berjalan sebagai root)Dua temuan dalam satu berkas: DATABASE_URL lengkap dengan passwordnya duduk di environment — terbaca tanpa satu pun sudo tambahan — dan field User yang kosong menegaskan prosesnya root. Tiga tempat yang benar untuk kredensial: build-time secret lewat RUN --mount=type=secret (nilai masuk memori build, bukan layer image), runtime file yang dipasang read-only ke container, atau secret manager untuk skala besar. Pola umumnya satu di semua tempat itu: kredensial disuntik saat proses berjalan, tidak pernah dibakar ke dalam image.
Hardening Runtime dan Checklist #
Setelah image bersih, giliran runtime memagari prosesnya. Tiga opsi docker run paling berdampak: --read-only membuat seluruh filesystem container baca-saja (tambahkan --tmpfs /tmp untuk berkas sementara), --cap-drop=ALL membuang seluruh capability kernel yang tidak diperlukan, dan --memory 256m --cpus 0.5 membatasi konsumsi sumber daya supaya satu container yang rusak tidak menyeret host-nya. Ringkasan seluruh praktik chapter ini:
| Lapisan | Praktik | Bentuknya |
|---|---|---|
| Image | pin versi dan digest | nginx:1.27@sha256:... |
| Dockerfile | proses non-root | USER node |
| Dockerfile | tanpa secret di layer | RUN --mount=type=secret |
| Runtime | filesystem baca-saja | --read-only --tmpfs /tmp |
| Runtime | buang capability | --cap-drop=ALL |
| Runtime | batas sumber daya | --memory 256m --cpus 0.5 |
| Supply chain | scan berkala | trivy image nginx:1.27 |
Digest pinning melengkapi pin versi: tag bisa digeser orang lain ke image berbeda, sedangkan @sha256:... mengunci byte persis yang kita audit. Scanning melengkapinya dari sisi waktu — image yang bersih hari ini bisa berisi CVE bulan depan, jadi pindai ulang secara berkala, bukan sekali saat build.
Dengan keamanan container, perlengkapan section ini lengkap: dari hypervisor dan VM, container dan compose, jaringan dan storage, hingga pagar kelilingnya. Peta perjalanan berikutnya berpindah medan — mesin Linux yang kita kelola jarang berada di meja kita sendiri; server di rak, VPS di cloud, Raspberry Pi di lemari. Section Remote Access dan Headless Linux dimulai dari mesin paling murni: yang bahkan tidak punya monitor — Headless Linux.
Referensi lanjut: Docker Engine security untuk pembahasan mendalam tiap lapisan, serta Build secrets untuk mekanisme secret saat build.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
inspect-web.json
Unduh berkas[
{
"Name": "/web-toko",
"Config": {
"Env": [
"DATABASE_URL=postgres://budi:CONTOH_PASSWORD_KUAT@db:5432/toko",
"PATH=/usr/local/bin:/usr/bin:/bin"
],
"User": ""
}
}
]