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:

bash
$ ls -l /var/run/docker.sock

Output yang Dihasilkan:

text
srw-rw---- 1 root docker 0 Oct 13 09:14 /var/run/docker.sock

Mari kita telaah izin pada baris tersebut:

  1. Huruf s di awal — jenis berkasnya socket unix, bukan berkas biasa; pembacaan-penulisannya berarti berbicara dengan daemon yang mendengarkan di sana.
  2. rw- untuk pemilik root — root selalu bisa bicara langsung ke daemon.
  3. rw- untuk grup docker — 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.
  4. --- untuk pihak lain — tanpa sudo dan tanpa keanggotaan grup docker, pintu ini tertutup rapat; pola izin 660 ini 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:

bash
$ docker run -it -v /:/host alpine:3.20 chroot /host sh

Output yang Dihasilkan (sesi interaktif, dipotong):

text
# whoami
root
# ls /home
budi
# hostname
server-latihan

Mari kita telaah apa yang barusan terjadi:

  1. -v /:/host memasang 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.
  2. chroot /host sh menggeser akar proses — shell yang berjalan kini membaca /bin, /etc, /home dari filesystem host; direktori /home/budi yang tampil adalah berkas Budi yang sungguhan.
  3. whoami menjawab 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.
  4. 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 --privileged yang 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:

yaml
services:
  agent-metrik:
    image: registry.example.com/agent-metrik:1.4
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Deklarasi 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 #

MitigasiYang dilakukanKapan dipilih
Tidak mount socket sama sekaliservice tidak menerima Docker APIpertanyaan pertama untuk setiap kasus
Socket-proxyAPI disaring default-deny di tengahagent/CI butuh akses terbatas
Rootless modedaemon berjalan tanpa rootworkstation dan development
API TCP dengan --tlsverifyakses remote dua arah terautentikasiadministrasi lintas mesin
DinD terisolasidaemon kedua dalam container ephemeralCI yang membangun kode tak dipercaya
Audit keanggotaan gruppemegang kunci dipetik dan dicabutkebersihan 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:

yaml
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:

  1. CONTAINERS=1 — hanya endpoint /containers yang dibukakan; daftar container dan statistiknya bisa dibaca agent, seluruh endpoint lain — image, volume, network, exec — tetap ditolak.
  2. POST=0 — kata kerja POST ditolak semua: agent bisa melihat keadaan daemon, tetapi tidak bisa menciptakan, mengubah, atau menghapus satu pun objek.
  3. Socket asli dipasang :ro — proxy sendiri membaca socket secara read-only; suffix :ro menutup jalur tulis yang secara teori tidak dibutuhkan siapa pun.
  4. 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.
  5. agent-net sebagai 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:

bash
$ getent group docker

Output yang Dihasilkan:

text
docker:x:998:budi,siti

Baris 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 #