Rumus pada Chapter Prinsip Keamanan Container menutup dengan satu kata yang murah tetapi jarang dilakukan: identitas. Sejak chapter pertama course ini, hampir semua container kita berjalan sebagai root — bukan karena keputusan sadar, melainkan karena Docker membiarkannya begitu: kecuali image-nya sendiri menurunkan identitas lewat instruksi USER, setiap container menyala sebagai user nomor 0. Chapter ini membedah kebiasaan bawaan tersebut, membangun pola penurunan identitas yang benar di Dockerfile, menimpanya dari sisi runtime dengan --user, lalu menyiapkan kita menghadapi tiga jebakan yang paling sering menjatuhkan tim yang baru pindah ke container non-root: volume milik root, port di bawah 1024, dan image resmi yang ternyata sudah menurunkan dirinya sendiri.
Default yang Mahal: Semua Berjalan sebagai Root #
Kita buktikan dulu bahwa default-nya memang begitu. Image nginx dijalankan tanpa aplikasi apa pun — kita ganti perintah bawaannya dengan whoami, pola command override yang sudah kita pegang sejak Section Dasar Docker CLI:
$ docker run --rm nginx:1.29 whoamiOutput yang Dihasilkan:
rootMari kita telaah satu kata root di atas dan rantai risiko yang dibawanya:
- Image nginx tidak memuat instruksi
USER— perintahwhoamiberjalan sebagai proses pertama container dengan identitas bawaan Docker: UID 0. Proses pengganti apa pun yang kita jalankan dari image ini mewarisi identitas yang sama. - Master nginx yang sesungguhnya pun berjalan sebagai root — image nginx melakukannya dengan sengaja: master root membuka port 80 dan membaca konfigurasi, lalu menurunkan setiap worker ke UID 101 lewat directive nginx sendiri; hanya pekerja yang turun, pengawasnya tetap root — pola yang sudah kita amati lewat
docker toppada Chapter User dan Permissions. - Rantai risikonya tersusun dari tiga mata rantai — pertama, bug pada aplikasi (misalnya path traversal atau eksekusi kode arbitrer) memberi penyerang shell di dalam container; kedua, shell itu mendarat sebagai UID 0 dengan dua belas capability bawaan, cukup untuk menulis backdoor ke writable layer, mengubah kepemilikan berkas, dan mengikat port mana pun; ketiga, penyerang tinggal menunggu celah runc atau kernel untuk melompat ke host — persis skenario WARNING pada chapter sebelumnya.
- Mata rantai termurah untuk diputus adalah yang pertama — shell hasil eksploitasi yang mendarat sebagai user biasa tidak bisa menulis ke berkas milik root, tidak bisa mengikat port rendah, dan daya rusaknya terkurung jauh lebih dini.
Menurunkan Identitas di dalam Dockerfile #
Pola penurunan yang sehat dikerjakan saat build, ketika kita masih memegang kendali penuh atas isi image. Kita bangun image latihan berbasis ubuntu:24.04 dengan satu user aplikasi bernama app:
FROM ubuntu:24.04
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd -g 1000 app && useradd -u 1000 -g app -m app
RUN mkdir -p /app && chown app:app /app
WORKDIR /app
USER app
CMD ["bash"]$ docker build -t aplikasi:nonroot .Output yang Dihasilkan:
[+] Building 14.2s (10/10) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 216B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:24.04 2.1s
=> [1/6] FROM docker.io/library/ubuntu:24.04 1.4s
=> => resolve docker.io/library/ubuntu:24.04 1.3s
=> [2/6] RUN apt-get update && apt-get install -y --no-install-re... 9.6s
=> [3/6] RUN groupadd -g 1000 app && useradd -u 1000 -g app -m app 0.4s
=> [4/6] RUN mkdir -p /app && chown app:app /app 0.3s
=> [5/6] WORKDIR /app 0.0s
=> [6/6] USER app 0.0s
=> exporting to image 0.4s
=> => exporting layers 0.3s
=> => writing image sha256:4e8a1c5f9b3d7e2a6c0f8d4b1e7a3c5f9b2d6e8a0c4f7b1e3d5a9c2f6b8d0e4a 0.0s
=> => naming to docker.io/library/aplikasi:nonroot 0.0sMari kita telaah Dockerfile di atas poin per poin:
- Angka UID dan GID ditulis eksplisit —
-g 1000pada groupadd dan-u 1000pada useradd mematok nomor secara pasti; kernel membandingkan angka, bukan nama, saat memeriksa izin berkas, sehingga nomor yang tetap membuat berkas di volume selalu cocok dengan pemiliknya. Konflik angka antara image dan volume adalah masalah klasik yang kita bedah tuntas pada Chapter Permission dan Ownership Volume. apt-getdiletakkan sebelumUSER— pemasangan paket menuntut hak root untuk menulis ke/var/lib/apt; seluruh pekerjaan ber-hak-root harus dikerjakan selagi build masih berjalan sebagai root.apt-getyang diletakkan setelahUSER appakan gagal dengan galat izin.WORKDIRdan kepemilikannya diurus sebelum turun — direktori yang dibuatWORKDIRselagi build masih root akan dimiliki root;mkdir -p /app && chown app:app /appmendahuluiWORKDIR /appsupaya userappberhak menulis di rumahnya sendiri. Untuk berkas aplikasi, padanannya adalahCOPY --chown=app:app . ..- Flag
-mpadauseraddmembuat home directory —/home/apptersedia untuk aplikasi yang menyimpan konfigurasi atau cache diHOME; tanpa-m, tool semacam git atau npm bisa gagal mencari rumahnya. - Kenapa angka, bukan sekadar nama —
useradd apptanpa-umenerima nomor otomatis berikutnya (misalnya 1000 atau 1001) tergantung isi image; nomor yang bisa bergeser antar build adalah bom waktu kepemilikan volume. Nama enak dibaca manusia, angka yang dipercaya kernel — kebiasaan yang kita sepakati sejak Chapter WORKDIR dan USER.
Identitas hasil build kita buktikan langsung dengan dua perintah pemeriksaan, plus satu uji tulis ke /app:
$ docker run --rm aplikasi:nonroot whoamiOutput yang Dihasilkan:
app$ docker run --rm aplikasi:nonroot idOutput yang Dihasilkan:
uid=1000(app) gid=1000(app) groups=1000(app)$ docker run --rm aplikasi:nonroot sh -c "touch /app/tanda && ls -l /app"Output yang Dihasilkan:
total 0
-rw-r--r-- 1 app app 0 Oct 13 10:12 tandaTiga jawaban itu menutup kontrak Dockerfile: proses berjalan sebagai app bernomor 1000, dan user tersebut benar-benar berhak menulis di /app — bukti bahwa urutan chown sebelum USER bekerja seperti dirancang.
Menimpa Identitas saat Runtime #
Instruksi USER pada image bukan kata terakhir. Flag --user (bentuk pendek -u) menimpa identitas proses utama container per jalannya — tanpa menyentuh image sama sekali, mekanisme yang kita kenal dari Chapter User dan Permissions:
$ docker run --rm --user 1234:1234 aplikasi:nonroot idOutput yang Dihasilkan:
uid=1234 gid=1234Timpaan bekerja dua arah, dan arah yang jarang disadari adalah naik:
$ docker run --rm --user 0:0 aplikasi:nonroot whoamiOutput yang Dihasilkan:
rootMari kita telaah dua arah timpaan tersebut:
--user 1234:1234menimpaUSER appdari image — proses berjalan sebagai UID 1234 meski Dockerfile menyebutapp; image tidak berubah sedikit pun.--user 0:0bisa menaikkan kembali ke root — penurunan di Dockerfile bukan tembok penjara bagi operator; siapa pun yang menjalankan container berhak menimpanya kembali.- Konsekuensi keamanannya jujur dan penting —
USERdi Dockerfile melindungi kita dari bug aplikasi: proses yang berhasil ditembus tetap mendarat sebagai user biasa.USERtidak melindungi kita dari operator yang tidak dipercaya — tetapi operator yang bisa menjalankandocker runtoh sudah memegang kendali setara root di host lewat docker.sock, pembahasan yang menunggu di Chapter Docker Socket Security.
Gotcha: Kernel Memakai Angka, Bukan Nama #
Menimpa identitas dengan angka yang tidak dikenal image memunculkan kejutan yang membingungkan pemula. Kita pakai image alpine yang tidak punya user bernomor 1000:
$ docker run --rm --user 1000:1000 alpine:3.20 whoamiOutput yang Dihasilkan:
whoami: unknown uid 1000$ docker run --rm --user 1000:1000 alpine:3.20 idOutput yang Dihasilkan:
uid=1000 gid=1000Mari kita telaah pasangan yang tampak bertentangan itu:
- Proses benar-benar berjalan sebagai UID 1000 — perintah
idmembuktikan angkanya; penimpaan identitas tidak gagal sama sekali. whoamigagal menerjemahkan, bukan kernel menolak menjalankan — toolwhoamimencari entri bernomor 1000 di/etc/passwdmilik image alpine, tidak menemukannya, lalu menyerah; keluarga galat semacam ini sudah kita kenali polanya pada Chapter Memahami Exit Code.- Solusinya persis pola Dockerfile di atas — daftarkan user di dalam image supaya angka punya nama, atau pilih angka yang memang sudah dikenal image. Aplikasi yang membaca
HOMEatau nama usernya sendiri adalah yang paling cepat tersandung gotcha ini.
Jebakan Volume Milik Root #
Penurunan identitas paling sering kandas di satu tempat: volume. Daemon Docker menciptakan volume baru dengan kepemilikan root:root, sehingga container non-root yang menulis ke sana ditolak mentah-mentah:
$ docker volume create data-ujiOutput yang Dihasilkan:
data-uji$ docker run --rm -v data-uji:/data alpine:3.20 touch /data/awal.txtPerintah kedua di atas berjalan sebagai root bawaan, sehingga awal.txt tercipta tanpa suara. Container berikutnya kita turunkan identitasnya lalu diminta menulis ke volume yang sama:
$ docker run --rm --user 1000:1000 -v data-uji:/data alpine:3.20 \
touch /data/tambahan.txtOutput yang Dihasilkan:
touch: /data/tambahan.txt: Permission deniedMari kita telaah penolakan yang sangat lazim itu:
- Volume segar dimiliki
root:rootdengan mode755— proses UID 1000 bukan pemiliknya dan bukan anggota grupnya, sehingga haknya tinggalr-xmilik pihak lain; menulis tidak termasuk. - Kegagalan muncul tanpa satu petunjuk akar masalah — pesan galat hanya menyebut
Permission denied; tidak ada kata “volume” atau “kepemilikan” — itulah sebabnya masalah ini kerap disalahkan ke aplikasi dan menghabiskan berjam-jam diagnosa. - Resep intinya tiga jalur — bila Dockerfile ada di tangan kita,
chownpath data ke UID aplikasi saat build atau pakaiCOPY --chown; bila image milik pihak ketiga, entry point berjalan sejenak sebagai root untuk membereskan kepemilikan lalu turun lewatgosuatausu-exec; bila data sudah telanjur dimiliki UID tertentu, jalankan container persis sebagai UID itu.
Ketiga jalur tersebut, beserta dua arah interaksi angka pemilik dan tabel diagnosis gejala, sudah kita bedah tuntas sampai ke perintahnya pada Chapter Permission dan Ownership Volume — chapter itu adalah rujukan lengkap setiap kali Permission denied muncul berdampingan dengan --user.
Port di Bawah 1024 #
Identitas non-root membawa satu aturan lama Linux ke dalam container: kernel melarang proses non-root mengikat port di bawah 1024 kecuali proses memegang capability NET_BIND_SERVICE. Aturan ini akan kita bahas penuh sebagai bagian dari peta capability pada chapter berikutnya; di sini cukup satu catatan praktis.
Image Resmi yang Menurunkan Diri Sendiri #
Sebagian besar image resmi besar tidak menunggu kita: mereka menurunkan identitas sendiri dengan pola dua tingkat — proses persiapan berjalan sebagai root, proses pekerjaan berjalan sebagai user rendah. Image nginx menurunkan worker ke UID 101 lewat directive user nginx; di konfigurasinya, seperti yang kita lihat pada kolom UID docker top di Chapter User dan Permissions. Entry point redis membetulkan kepemilikan direktori datanya dulu, lalu turun ke user redis. Entry point postgres menyiapkan direktori data sebagai root, kemudian berpindah ke user postgres bernomor UID 999 lewat gosu — alat penurunan wewenang khas keluarga Debian yang sudah kita temui pada Chapter Permission dan Ownership Volume.
Implikasinya dua arah. Pertama, sebelum menambahkan --user pada image resmi, periksa dulu dokumentasinya — menimpa identitas yang sudah diturunkan dengan angka yang salah justru menciptakan masalah izin baru, persis skenario Permission denied di atas. Kedua, untuk aplikasi kita sendiri, pola dua tingkat itu layak ditiru bila inisialisasi memang menuntut root: entry point root membereskan berkas, lalu exec gosu app menyerahkan tongkat ke user aplikasi.
Pemetaan User Namespace #
Ada satu teknik penurunan yang bekerja satu tingkat lebih dalam daripada USER dan --user: user namespace remap, konfigurasi daemon yang memetakan ulang seluruh UID container ke rentang UID tinggi di host. Dengan remap aktif, root container (UID 0 di dalam) diterjemahkan host sebagai user biasa bernomor tinggi — misalnya 100000 — sehingga berkas yang ditulis container tampak milik 100000 di sisi host; sekalipun ada penyerang yang “lolos” sebagai root container, ia mendarat di host sebagai user tanpa privilege apa pun. Konfigurasi ini berlaku daemon-wide untuk semua container dan berkerabat erat dengan mode rootless yang akan kita jumpai lagi pada Chapter Docker Socket Security; untuk sebagian besar tim, USER plus --user di level image sudah menutup kebutuhan sehari-hari.
Tiga Jalan Penurunan #
| Cara | Bekerja pada | Paling pas untuk |
|---|---|---|
USER di Dockerfile | build — menempel permanen di image | image aplikasi milik sendiri |
--user saat docker run | create — per container, menimpa image | image pihak ketiga yang belum menurunkan |
distroless :nonroot | build — UID 65532, tanpa root dan tanpa shell | image produksi minimal |
Identitas kini berada di bawah kendali kita: image aplikasi sendiri menurunkan sejak build, image orang lain bisa kita timpa saat runtime, dan jebakan volume maupun port sudah punya peta jalannya. Tetapi rumus pada chapter pembuka masih menyisakan suku yang lebih besar: kekuatan root yang tersisa itu sendiri. Root di dalam container masih membawa dua belas capability — unit-unit privilege seperti CHOWN, SETUID, dan NET_BIND_SERVICE — dan sebagian besar aplikasi nyata hanya membutuhkan satu-dua di antaranya. Chapter Linux Capabilities membedah peta lengkapnya: apa saja yang dibawa container bawaan, bagaimana mencabut semuanya, lalu menambahkan kembali hanya yang benar-benar dipakai aplikasi.
Referensi #
- Dockerfile Reference
— definisi resmi instruksi
USERbeserta aturan nama dan angka UID. - docker container run — CLI Reference
— referensi flag
--userdengan bentuk uid, uid:gid, dan nama. - Dockerfile best practices — panduan resmi pola membuat dan memakai user non-root.
- Docker security — kerangka isolasi tempat penurunan identitas menjadi salah satu lapisannya.