Hardening Container Docker: Non-Root, Image Kecil, Scanning
Hardening container Docker langkah demi langkah: user non-root, base image minimal, port loopback, batas resource, dan scan CVE, dicek pada setup nyata.
Oleh Matthews WongDitinjau pada
Poin utama
Hardening container berarti memperkecil apa yang bisa dipakai penyerang jika aplikasi Anda berhasil diretas. Dasarnya adalah base image kecil, user non-root, port yang hanya terikat di tempat yang perlu, batas resource, dan scan image untuk kerentanan yang sudah dikenal. Tidak ada satu kontrol yang cukup, dan setup nyata biasanya punya celah yang layak disebut.
Setelah pelajaran ini, Anda bisa
- Menyebutkan lapisan hardening container dan mana saja yang benar-benar diatur file Docker situs ini.
- Mengikat port yang dipublikasikan ke loopback, dan menjelaskan kenapa EXPOSE saja tidak mempublikasikan apa pun.
- Memindai image dengan Docker Scout atau Trivy dan membuat pipeline gagal pada temuan serius.
Ikuti diagramnya langkah demi langkah
Tekan Putar, atau melangkah dengan panah. Setiap langkah juga ditulis di bawah diagram.
Lapisan di sekeliling satu container
Lima kontrol menumpuk di sekeliling container: image kecil, proses non-root, port khusus loopback, batas resource dengan health check, dan scan kerentanan pada image, yang tidak dijalankan repo ini.
Tombol: ← → langkah · Home / End · K putar atau jeda
Pengunjung mencapai container lewat proxy dan sebuah port. Di sekeliling container itu ada kontrol yang dibahas pelajaran ini, satu lapisan demi satu lapisan.
Mulai dari base minimal, di sini Alpine, dan biarkan multi-stage build hanya menyalin tiga folder. Software yang lebih sedikit di image berarti lebih sedikit yang perlu ditambal dan dipindai.
FROM node:20-alpine AS runnerServer berjalan sebagai system user dengan uid 1001, bukan root. Jika aplikasi diretas, penyerang memulai dengan akun tanpa hak istimewa.
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs USER nextjsTebak langkah berikutnya File compose mempublikasikan port aplikasi. Ke alamat host mana ia terikat? (Semua interface host · Hanya loopback)Port terikat ke 127.0.0.1, sehingga hanya proxy di mesin yang sama yang menjangkaunya. Pengunjung yang mencoba port itu langsung dari luar tidak menemukan apa pun.
ports: bound to 127.0.0.1 onlyContainer boleh memakai memori 512 MB dan satu CPU, dan health check memeriksanya setiap 30 detik. Aplikasi yang lepas kendali terkurung dan yang mati ketahuan.
mem_limit: 512m cpus: "1.0"Tebak langkah berikutnya Dengan apa pemindai kerentanan membandingkan isi sebuah image? (Daftar port terbuka di host · Database kerentanan yang sudah dikenal)Pemindai mendaftar komponen image dan mencocokkannya dengan database kerentanan. Repo ini tidak menjalankannya; ini lapisan yang perlu ditambah berikutnya, dan garis putus-putus menunjukkan tempatnya.
docker scout cves --only-severity critical,high --exit-code
Apa sebenarnya arti hardening container?
Hardening bukan satu tombol. Ia adalah kumpulan keputusan kecil yang masing-masing menghilangkan sesuatu yang bisa dipakai penyerang setelah masuk ke aplikasi Anda: software yang bisa disalahgunakan, hak istimewa yang bisa diwarisi, jaringan yang bisa dijangkau, resource yang bisa dihabiskan, dan celah dikenal yang bisa dieksploitasi. Panduan keamanan Docker sendiri menyebut root di dalam container jauh lebih rendah hak istimewanya daripada root di host, dan Docker cukup aman terutama jika proses Anda dijalankan sebagai user tanpa hak istimewa di dalam container.
Baca selengkapnya (1)Tutup
Kalimat itu juga peringatan agar tidak berhenti di tengah. Menurunkan hak istimewa membuat kerusakan serius lebih sulit, bukan mustahil, jadi kontrol ditumpuk. Pelajaran ini menelusuri tumpukan itu pada file situs ini sendiri dan menandai tiap lapisan sebagai sudah atau belum diatur, karena daftar celah yang jujur lebih berguna daripada klaim sudah aman.
Kenapa menjalankan container sebagai user non-root?
Proses container berjalan sebagai user yang ditentukan image. Referensi Compose lugas soal ini: jika image tidak menetapkan user, container berjalan sebagai root. Panduan best practice Docker memberi aturannya dalam satu kalimat: jika sebuah service bisa berjalan tanpa hak istimewa, pakai USER untuk beralih ke user non-root. Stage runner situs ini membuat system user dengan uid 1001, menyalin hasil build dengan pemilik itu, lalu beralih dengan USER.
Baca selengkapnya (1)Tutup
Pelajaran multi-stage membahas mekanisme COPY dan kepemilikan. Yang penting di sini adalah posisinya dalam tumpukan: ia membatasi apa yang bisa dilakukan aplikasi yang diretas di dalam container, dan tidak ada biaya saat runtime. Ini lapisan termurah, itulah sebabnya ia datang sebelum yang lebih sulit.
Seberapa kecil base image sebaiknya?
Panduan Docker adalah membangun dari sumber tepercaya dan menjaga image tetap kecil, memilih base minimal yang sesuai kebutuhan, dan ia menyebut Alpine sebagai image yang terkontrol ketat dan kecil. Ia juga menyarankan image yang lebih lengkap untuk build dan yang lebih ramping untuk production, persis seperti yang diberikan multi-stage build. Dockerfile ini memakai node:20-alpine di setiap stage dan hanya mengirim tiga folder dari builder.
Baca selengkapnya (2)Tutup
File .dockerignore adalah bagian yang lebih sunyi dari gagasan yang sama. Ia menjaga .git, .env, dan .env.* di luar build context, sehingga sejak awal tidak ikut disalin ke builder oleh COPY . . . Secret yang tidak pernah masuk ke sebuah stage tidak bisa bocor darinya.
Dua batas yang jujur. Dockerfile ini mengunci tag, bukan digest, dan panduan Docker menyebut Anda bisa mengunci image ke digest tertentu untuk mengamankan supply chain, serta sebaiknya rebuild secara rutin untuk mendapat dependency terbaru. Tag lebih mudah dijaga tetap baru; digest lebih mudah dipercaya. Mana pun yang dipilih, rebuild adalah cara perbaikan sampai.
Apa yang ditambahkan file Compose di sekeliling container?
Port yang dipublikasikan terikat ke 127.0.0.1 di host. Referensi Compose memperingatkan bahwa port yang dipublikasikan tanpa alamat host memakai semua interface dan bisa mengekspos container langsung ke internet jika host punya alamat publik. Mengikat ke loopback berarti hanya sesuatu di mesin yang sama, di sini reverse proxy, yang bisa menjangkaunya. Perhatikan bahwa EXPOSE 3000 di Dockerfile hanyalah dokumentasi: Docker menyatakan ia tidak mempublikasikan port.
Baca selengkapnya (2)Tutup
mem_limit membatasi memori yang boleh dialokasikan container dan cpus membatasi CPU yang boleh dipakai, di sini 512 MB dan satu. Health check menjalankan wget ke /robots.txt setiap 30 detik agar aplikasi yang mati ketahuan, dan log driver json-file dibatasi tiga file masing-masing 10 MB agar log tidak memenuhi disk.
Sekarang celahnya. Referensi Compose mendokumentasikan read_only untuk filesystem hanya-baca, cap_drop untuk menghapus Linux capabilities, dan security_opt untuk opsi seperti no-new-privileges. File ini tidak menyetel satu pun, dan juga tidak menimpa user, karena image sudah menetapkannya. Itu langkah hardening berikutnya, bukan hal yang boleh dianggap sudah beres.
Bagaimana memindai image untuk kerentanan yang sudah dikenal?
Pemindai membuat inventaris isi sebuah image, yaitu software bill of materials, dan mencocokkannya dengan database kerentanan yang terus diperbarui. Docker Scout melakukannya dari Docker CLI: docker scout cves menganalisis image, --only-severity menyaring tingkat seperti critical dan high, dan --exit-code membuat perintah mengembalikan exit code 2 saat kerentanan ditemukan, yang membuat job CI gagal. Tanpa argumen artifact, ia menganalisis image yang paling baru di-build.
Baca selengkapnya (2)Tutup
Trivy adalah alternatif open-source yang umum: trivy image diikuti nama image. Menurut dokumentasinya, pemindaian kerentanan dan secret aktif secara default, sedangkan pemindaian lisensi nonaktif sampai Anda memintanya dengan --scanners.
Repository ini tidak menjalankan keduanya: tidak ada pemindai di Dockerfile, file compose, maupun dua workflow-nya. Itu temuan, bukan kegagalan pelajaran ini. Karena database terus diperbarui, image yang bersih bulan lalu bisa melaporkan temuan hari ini, jadi scan sebaiknya ada di pipeline dan berjalan terjadwal, bukan dijalankan sekali.
Konfigurasi nyata di balik pelajaran ini
Kutipan dari repositori yang membangun dan men-deploy situs ini, apa adanya.
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjsSeluruh setup non-root hanya satu baris ini ditambah USER nextjs di baris 64. Grup dan user-nya sama-sama system account. Id numerik tetap 1001 muncul lagi di docker-compose.yml, yang komentarnya mengandalkannya untuk menjelaskan siapa pemilik named volume.
node_modules
.git
.env
.env.*
.DS_Store
npm-debug.log*Baris-baris awal file ignore. .git, .env, dan .env.* tidak pernah masuk ke build context, jadi COPY berikutnya tidak bisa membawa riwayat atau secret ke layer image. node_modules juga dikecualikan, karena builder mengambilnya dari stage deps.
ports:
# Bind to loopback only — nginx terminates TLS and proxies to this port.
- "127.0.0.1:${APP_PORT:-3002}:3000"
mem_limit: 512m
cpus: "1.0"Tiga keputusan hardening dalam lima baris: siapa yang bisa menjangkau aplikasi, berapa memori yang boleh diambil, dan berapa CPU. Komentarnya menyebut alasan pengikatan loopback. Komentar NODE_OPTIONS di Dockerfile menjelaskan bahwa batas heap Node harus tetap di bawah batas memori ini.
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://127.0.0.1:3000/robots.txt",
]
interval: 30s
timeout: 5s
retries: 3
start_period: 20sHealth check mengubah kegagalan senyap menjadi status yang terlihat. Ia memeriksa halaman ringan dari dalam container, jadi tidak butuh akses dari luar. Ia memberi tahu bahwa aplikasi berhenti menjawab; ia tidak memberi tahu bahwa aplikasi telah diretas.
Kesalahan yang benar-benar membuang waktu
Port yang dipublikasikan di semua interface
Menulis 3000:3000 tanpa alamat host mengikat semua interface. Pada host dengan alamat publik, aplikasi lalu bisa dijangkau langsung dari internet, melewati proxy, rate limit, dan TLS-nya. Tulis 127.0.0.1:3000:3000 jika hanya proxy lokal yang membutuhkannya.
Mengandalkan EXPOSE untuk menutup atau membuka port
EXPOSE hanya mendokumentasikan port untuk orang yang membaca image. Ia tidak mempublikasikan maupun melindungi port. Publikasi terjadi lewat -p atau entri ports, dan perlindungan datang dari tempat Anda mengikat serta apa yang menyaring lalu lintas sebelumnya.
Scan yang hanya melapor dan tidak pernah memblokir
Pemindai yang hanya mencetak temuan ke log yang tidak dibaca siapa pun tidak melindungi apa-apa. Pakai opsi exit code agar temuan serius menggagalkan job, saring menurut severity agar gerbangnya tetap layak dipakai, dan pindai ulang secara terjadwal, karena database kerentanan terus berubah setelah image dibuat.
Dikunci tetapi tidak pernah di-rebuild
Mengunci base image ke digest membuat build dapat direproduksi sekaligus membekukan Anda pada celahnya. Panduan Docker memasangkan penguncian dengan rebuild rutin untuk dependency terbaru. Jika Anda mengunci, jadwalkan rebuild-nya; jika memakai tag, terima bahwa baris yang sama bisa berarti isi berbeda bulan depan.
| Lapisan | Yang dibatasi | Di repo ini |
|---|---|---|
| Base minimal, multi-stage | Software di dalam image | Ada: alpine, tiga folder |
| USER non-root | Hak istimewa di dalam container | Ada: uid 1001 |
| Port loopback | Siapa yang bisa menjangkau aplikasi | Ada |
| Batas memori dan CPU | Kehabisan resource | Ada |
| read_only, cap_drop, no-new-privileges | Penulisan, capabilities, kenaikan hak istimewa | Belum ada |
| Scan kerentanan image | Celah dikenal pada komponen | Belum dipasang |
Lakukan di komputer Anda sendiri
Anda butuh Docker. Anda akan memeriksa user yang menjalankan container, membuat image non-root, memperketat cara menjalankannya, lalu memindainya.
Tanya sebuah image bawaan, siapa yang menjalankannya. Jika tercetak uid=0, prosesnya adalah root.
docker run --rm node:20-alpine idTulis image kecil yang membuat user tanpa hak istimewa lalu beralih ke user itu, kemudian build.
# Dockerfile FROM node:20-alpine RUN addgroup --system --gid 1001 app && adduser --system --uid 1001 -G app app USER app CMD ["node", "-e", "setInterval(() => {}, 1000)"] # shell docker build -t hardening-lab . docker run --rm hardening-lab idJalankan lewat Compose dengan filesystem hanya-baca, tanpa Linux capabilities, dan tanpa kenaikan hak istimewa, lalu pastikan ia berjalan.
# compose.yaml services: lab: image: hardening-lab read_only: true cap_drop: - ALL security_opt: - no-new-privileges:true mem_limit: 128m ports: - "127.0.0.1:3000:3000" # shell docker compose up -d docker compose psPindai image yang baru dibuat, hanya tampilkan temuan critical dan high, lalu baca exit code. Jika Trivy terpasang, trivy image hardening-lab melakukan hal yang sama.
docker scout cves --only-severity critical,high --exit-code echo $?
Tiga pertanyaan sebelum lanjut
Pilih jawaban untuk melihat kenapa benar atau salah. Tidak ada yang disimpan atau dinilai, jadi tebak saja dengan bebas.
Jawab dalam hati, lalu cek
1.Kontrol hardening apa yang diatur file compose situs ini, dan apa yang tidak?Lihat jawabannya
Jawaban:File itu mengatur port khusus loopback, batas memori, batas CPU, health check, dan log yang dibatasi. Ia tidak mengatur read_only, cap_drop, atau no-new-privileges. User non-root berasal dari Dockerfile, bukan dari Compose.
2.Dockerfile menulis EXPOSE 3000. Apakah itu mempublikasikan port?Lihat jawabannya
Jawaban:Tidak. EXPOSE mendokumentasikan port. Publikasi dilakukan oleh entri ports di Compose atau -p pada docker run, dan entri situs ini mengikatnya hanya ke 127.0.0.1.
3.Apakah image situs ini dipindai kerentanannya di pipeline-nya?Lihat jawabannya
Jawaban:Tidak dalam apa pun yang ada di repository ini: Dockerfile, file compose, dan dua workflow tidak merujuk pemindai apa pun. Docker Scout atau Trivy adalah lapisan berikutnya yang perlu ditambah, dengan exit code agar temuan serius menggagalkan job.
Bahasan mendalam terkait di situs ini
Sumber
Sumber primer yang diperiksa pada tanggal riset. Ikuti tautannya untuk versi terbaru.