Rantai pasok image kini terkunci dari source sampai deploy, dan kelima lapisan isolasi container tegak dari syscall sampai filesystem. Satu lubang terakhir yang menutup section ini justru lubang yang paling sering diabaikan, karena letaknya bukan di dalam container melainkan pada sistem yang menjalankan semuanya: socket Docker sendiri. Hampir setiap latihan sepanjang course ini berbicara ke daemon lewat satu pintu — /var/run/docker.sock — tanpa pernah bertanya apa sebenarnya pintu itu pegangi. Jawabannya singkat dan mengejutkan: siapa pun yang memegang socket itu memegang root di host, dan pola yang tampak tidak berbahaya seperti memasang socket ke dalam container justru menyerahkan kunci kerajaan itu begitu saja. Chapter penutup section ini membongkar daya socket lewat satu demonstrasi edukatif, memetakan pola berisiko yang nyata di CI dan agent pemantauan, lalu merakit enam mitigasi — dari socket-proxy sampai rootless — untuk menutupnya kembali.
Socket yang Menjalankan Segalanya #
Perintah docker ps yang kita ketik sepanjang course ini sebenarnya adalah panggilan REST API: klien Docker menyusun permintaan HTTP, mengirimkannya lewat socket unix /var/run/docker.sock, dan daemon yang mendengarkan di ujung sana mengerjakannya. Socket itu adalah satu-satunya pintu menuju seluruh kemampuan daemon — membuat container, mem-mount direktori host, menarik image dari mana pun. Siapa yang boleh membukanya diatur oleh izin berkas Unix biasa:
$ ls -l /var/run/docker.sockOutput yang Dihasilkan:
srw-rw---- 1 root docker 0 Oct 13 09:14 /var/run/docker.sockMari kita telaah izin pada baris tersebut:
- Huruf
sdi awal — jenis berkasnya socket unix, bukan berkas biasa; pembacaan-penulisannya berarti berbicara dengan daemon yang mendengarkan di sana. rw-untuk pemilikroot— root selalu bisa bicara langsung ke daemon.rw-untuk grupdocker— seluruh anggota grup docker memegang hak baca-tulis yang setara pemilik; inilah satu-satunya jalur akses tanpa sudo, mekanisme yang kita pasang pada Chapter Mengatur Akses Docker.---untuk pihak lain — tanpa sudo dan tanpa keanggotaan grup docker, pintu ini tertutup rapat; pola izin660ini adalah desain yang disengaja.
Membuktikan Dayanya: Shell Root dalam Satu Perintah #
Klaim “socket setara root” terasa berlebihan sampai kita melihat pola penyerangannya. Demonstrasi berikut bersifat edukatif — tujuannya membuat kita mengenali polanya, bukan menirunya di mesin kerja:
$ docker run -it -v /:/host alpine:3.20 chroot /host shOutput yang Dihasilkan (sesi interaktif, dipotong):
# whoami
root
# ls /home
budi
# hostname
server-latihanMari kita telaah apa yang barusan terjadi:
-v /:/hostmemasang akar filesystem host — bind mount persis mekanisme Chapter Bind Mount yang kita pakai untuk berbagi source code; bedanya, sumber yang dipasang kali ini adalah/milik host, seluruh isi mesin.chroot /host shmenggeser akar proses — shell yang berjalan kini membaca/bin,/etc,/homedari filesystem host; direktori/home/budiyang tampil adalah berkas Budi yang sungguhan.whoamimenjawab root dengan izin nyata — proses berjalan sebagai UID 0 dan, tanpa user namespace remap, UID 0 container adalah UID 0 host terhadap berkas yang terpasang: berkas Budi bisa dibaca, diubah, dihapus.- Tidak ada lapisan Docker yang melawan — mount seperti ini sah bagi daemon, karena perintahnya datang dari pemegang sah socket; inilah alasan pola serangan “cukup berikan akses docker run” identik dengan pola breakout
--privilegedyang kita bedah pada Chapter Privileged Container.
Pola Berisiko di Dunia Nyata #
Dua keluarga pola nyata hampir selalu ditemui di lapangan, dan keduanya tampak tidak berbahaya justru karena niatnya baik. Yang pertama: CI runner — GitLab Runner, Jenkins, dan sejenisnya — yang perlu membangun image Docker di dalam job-nya. Yang kedua: agent pemantauan yang menjanjikan metrik container lengkap dengan satu syarat kecil di dokumentasinya:
services:
agent-metrik:
image: registry.example.com/agent-metrik:1.4
volumes:
- /var/run/docker.sock:/var/run/docker.sockDeklarasi satu baris itu adalah penyerahan kunci: agent-metrik kini berbicara langsung ke daemon host. Untuk mengumpulkan metrik, agent hanya butuh membaca daftar container dan statistiknya — tetapi socket yang dipasang tidak bisa “setengah terbuka”; API yang tersedia adalah API penuh, termasuk hak menciptakan container --privileged seperti demo di atas.
Docker-in-Docker versus Socket Sibling #
Dua rancangan berbagi kebutuhan “Docker di dalam container”, dan keduanya sudah kita kenali dari Chapter Privileged Container. Docker-in-Docker (DinD) menjalankan daemon kedua di dalam container docker:dind — bersih dan terisolasi dari daemon host, tetapi menuntut --privileged penuh dan membawa biaya kompleksitasnya sendiri. Pola sibling memakai image docker:29-cli yang hanya membawa klien dan berbicara ke daemon host lewat socket — ringan dan tanpa --privileged, tetapi justru karena itulah pola ini menyerahkan socket host ke dalam container. Trade-off-nya jujur: bila yang dibangun adalah kode tidak dipercaya, DinD pada runner ephemeral lebih tepat karena peledakan terbatas pada daemon bagian dalam; bila yang dibutuhkan hanya operasi ringan di daemon host, pola sibling layak dipertahankan asalkan socket-nya disaring — dan untuk itulah socket-proxy ada.
Enam Mitigasi #
| Mitigasi | Yang dilakukan | Kapan dipilih |
|---|---|---|
| Tidak mount socket sama sekali | service tidak menerima Docker API | pertanyaan pertama untuk setiap kasus |
| Socket-proxy | API disaring default-deny di tengah | agent/CI butuh akses terbatas |
| Rootless mode | daemon berjalan tanpa root | workstation dan development |
API TCP dengan --tlsverify | akses remote dua arah terautentikasi | administrasi lintas mesin |
| DinD terisolasi | daemon kedua dalam container ephemeral | CI yang membangun kode tak dipercaya |
| Audit keanggotaan grup | pemegang kunci dipetik dan dicabut | kebersihan berkelanjutan |
Baris pertama tabel itu bukan kalimat basa-basi: banyak agent pemantauan sebenarnya punya mode tanpa socket — cgroup dan log terbaca lewat jalur lain — sehingga pilihan termurah adalah tidak memberikan apa pun.
Dua baris lain pada tabel mengubah sisi daemon, bukan sisi container. Rootless mode menjalankan daemon sebagai user biasa tanpa satu pun hak root: pemegang socket yang dibajak mendarat sebagai user biasa, bukan sebagai root host. Opsi API TCP dengan --tlsverify menjawab kebutuhan yang berbeda — akses daemon dari mesin lain melalui TLS dua arah dengan sertifikat klien dan server — konfigurasi daemon.json yang sudah kita rakit pada Chapter Mengatur Akses Docker.
Socket-Proxy: Menyaring API di Tengah #
Bila agent memang harus membaca data Docker, socket-proxy menyisipkan penjaga di antara agent dan daemon. Image tecnativa/docker-socket-proxy menjalankan reverse proxy mungil yang membaca socket asli lalu hanya meneruskan endpoint yang kita izinkan secara eksplisit — default-nya, semua ditolak:
services:
socket-proxy:
image: tecnativa/docker-socket-proxy
environment:
- CONTAINERS=1
- POST=0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- agent-net
agent-metrik:
image: registry.example.com/agent-metrik:1.4
environment:
- DOCKER_HOST=tcp://socket-proxy:2375
networks:
- agent-net
networks:
agent-net:Mari kita telaah lima keputusan pada compose tersebut:
CONTAINERS=1— hanya endpoint/containersyang dibukakan; daftar container dan statistiknya bisa dibaca agent, seluruh endpoint lain — image, volume, network, exec — tetap ditolak.POST=0— kata kerja POST ditolak semua: agent bisa melihat keadaan daemon, tetapi tidak bisa menciptakan, mengubah, atau menghapus satu pun objek.- Socket asli dipasang
:ro— proxy sendiri membaca socket secara read-only; suffix:romenutup jalur tulis yang secara teori tidak dibutuhkan siapa pun. DOCKER_HOST=tcp://socket-proxy:2375— klien di dalam agent-metrik tidak pernah melihat socket host; alamat satu-satunya yang diketahuinya adalah proxy di network agent-net.agent-netsebagai network terpisah — proxy dan agent hidup di jaringan mereka sendiri; container lain di stack tidak bisa ikut mencoba-coba proxy, dan socket host tidak pernah keluar dari mesin.
Hasil akhirnya mengubah ancaman menjadi terukur: agent yang dibajak penuh sekalipun hanya bisa membaca daftar container — tidak bisa menciptakan shell root seperti demo di awal chapter.
Audit Keanggotaan Grup #
Mitigasi terakhir tidak menyentuh teknologi sama sekali, tetapi menutup lubang yang paling manusiawi: daftar siapa saja yang memegang kunci:
$ getent group dockerOutput yang Dihasilkan:
docker:x:998:budi,sitiBaris itu mencetak nama grup, GID 998, dan anggotanya: budi dan siti — dua orang yang memegang setara root di host ini. Audit berkala membandingkan daftar itu dengan daftar orang yang memang seharusnya memegangnya; mantan kolega, akun service yang terlupakan, dan skrip instalasi lama adalah penghuni liar yang paling sering ditemukan. Pencabutannya satu perintah: sudo gpasswd -d nama-user docker — efektif pada login berikutnya.
Lima lapisan isolasi kini kita kenal dari dalam ke luar, dan section ini menegakkannya lapis demi lapis: container berjalan non-root tanpa capability liar di atas filesystem beku, syscall tersaring seccomp dan LSM, rahasia naik dari teks polos menjadi objek terkelola, image dipindai, dibuktikan asal-usulnya, dan dirangkai dalam rantai pasok terkunci. Pada ujung semua itu berdiri kebenaran paling sederhana sepanjang section: keamanan container berakhir pada siapa yang boleh bicara dengan daemon — dan jawabannya harus sesedikit mungkin orang. Dengan seluruh bekal ini, image yang aman dan terverifikasi tinggal satu langkah lagi sebelum benar-benar melayani dunia: didistribusikan. Section Registry dan Distribusi membuka babak itu — bagaimana image berpindah dari mesin build ke registry, dari registry ke server, dan bagaimana menjaga identitasnya di sepanjang perjalanan.
Referensi #
- Rootless mode — menjalankan daemon Docker tanpa root sebagai mitigasi fundamental.
- Protect the Docker daemon socket — mengamankan akses socket dan API daemon dengan TLS.
- Docker security — kerangka keamanan Docker tempat socket berada di pusatnya.