Flag --privileged pada chapter sebelumnya melepaskan empat pembatas sekaligus; sekarang kita bergerak ke kutub yang berlawanan — pembatas yang ditegakkan sampai ke butir terakhir. Sepanjang course ini, setiap tulisan yang dilakukan container — berkas baru, backdoor yang dijatuhkan penyerang, cache yang menumpuk — selalu mendarat di satu tempat: writable layer di puncak tumpukan image, mekanisme yang kita bedah tuntas pada Chapter Writable Container Layer. Chapter ini membahas flag yang membuang lapisan itu seluruhnya: --read-only membekukan akar filesystem container sehingga tidak ada satu path pun yang bisa ditulis — oleh user biasa, oleh root, sekalipun oleh proses dengan seluruh capability. Aplikasi nyata tetap butuh menggores di beberapa titik, jadi kita belajar memilih lubang tulis yang diberikan secara sengaja lewat tmpfs, lalu menutup chapter dengan kombinasi emas yang merangkai seluruh pelajaran section ini menjadi satu perintah.

Membuang Writable Layer Seluruhnya #

Tanpa flag tambahan, container lahir dengan writable layer yang siap menampung semua tulisan. Dengan --read-only, Docker memasang seluruh rootfs — image beserta tumpukan layernya — sebagai read-only mount: writable layer tidak disediakan, dan setiap upaya menulis ditolak kernel dengan galat EROFS (Error: Read-Only File System). Bukti paling ringkasnya cuma satu baris:

bash
$ docker run --rm --read-only alpine:3.20 touch /uji

Output yang Dihasilkan:

text
touch: /uji: Read-only file system

Penolakan itu berlaku tanpa memandang identitas — proses root pada container --read-only menemui pesan yang sama. Sebelum flag ini dipasang pada aplikasi sungguhan, ada satu kejujuran yang harus kita terima lebih dulu: aplikasi nyata menulis lebih banyak daripada yang kita kira.

Aplikasi Nyata Butuh Menulis #

Chapter Mount Read-Only pernah memperingatkan bahwa kombinasi penguncian ini menuntut pemetaan kebutuhan tulis lebih dulu — dan pemetaan itu bukan teori. Nginx tanpa area tulis akan menolak menyala dengan galat nginx: [emerg] mkdir() /var/cache/nginx/client_temp failed (30: Read-only file system); aplikasi Node menulis pid file; runtime bahkan yang paling sopan pun kerap menyentuh /tmp.

Kebutuhan aplikasiSolusi pada pola read-only
Berkas sementara--tmpfs /tmp — hidup di RAM, lenyap saat container berhenti
Cache internal--tmpfs pada path cache, lengkap dengan kuota size
Berkas pid dan runtime--tmpfs /run — pola baku service Unix
Log aplikasiarahkan ke stdout agar terbaca docker logs — pola Chapter Docker Logs
Data yang harus bertahanvolume dipasang rw — mount tetap dapat ditulis walau rootfs beku
Konfigurasidibakal ke image, atau bind mount :ro seperti Chapter Mount Read-Only

Dua baris terakhir tabel adalah pengecualian yang sering terlewat: --read-only membekukan akar filesystem, tetapi volume dan bind mount yang dipasang setelahnya membawa kebijakan izinnya sendiri — volume rw tetap bisa ditulis, dan penguncian per-mount tetap diatur suffix :ro sebagaimana dibedah pada Chapter Mount Read-Only.

Resep Lengkap: nginx dengan Dua Lubang tmpfs #

Sekarang kita rakit nginx sungguhan di atas filesystem beku. Dua lubang tulis dibuka dengan sengaja: /var/cache/nginx untuk direktori kerja internalnya, dan /run untuk berkas pid:

bash
$ docker run -d --name web-kekal --read-only \
    --tmpfs /var/cache/nginx:rw,noexec,nosuid,size=16m \
    --tmpfs /run \
    -p 8083:80 \
    nginx:1.29

Output yang Dihasilkan:

text
c4e8a1f7b3d59c2e6a0f4b8d1c5e7a3f9b0d2c4e6a8f1b3d5c7e9a0f2b4d6c8e

Mari kita telaah opsi yang ditempelkan pada tmpfs pertama:

  1. rw — lubang ini boleh ditulis; inilah alasan lubang dibuka sama sekali.
  2. noexec — berkas yang ditulis ke sini tidak boleh dieksekusi sebagai program; payload yang dijatuhkan penyerang bisa mendarat tetapi tidak bisa dijalankan dari lokasi ini.
  3. nosuid — bit setuid pada berkas apa pun di lubang ini diabaikan kernel; trik eskalasi klasik lewat berkas setuid hasil unduhan langsung mati.
  4. size=16m — kuota RAM untuk lubang ini; aplikasi yang liar menulis cache dihentikan oleh kuota, bukan oleh RAM host yang habis.
  5. /run dipasang polos — Docker memasang tmpfs tanpa opsi dengan pengaturan bawaan yang setara — rw, noexec, nosuid — dan batas ukuran 64 MB; format opsi yang sama dengan /var/cache/nginx, cukup dengan ukuran berbeda.

Tiga verifikasi menutup resep: akar beku, lubang hidup, dan layanan melayani.

bash
$ docker exec web-kekal touch /berkas-uji

Output yang Dihasilkan:

text
touch: cannot touch '/berkas-uji': Read-only file system
bash
$ docker exec web-kekal touch /var/cache/nginx/uji
$ docker exec web-kekal ls /var/cache/nginx

Output yang Dihasilkan:

text
client_temp  fastcgi_temp  proxy_temp  scgi_temp  uji  uwsgi_temp
bash
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8083

Output yang Dihasilkan:

text
200

Mari kita telaah ketiga verifikasi tersebut:

  1. Tulisan ke akar filesystem ditolak — touch /berkas-uji gagal meski docker exec bawaan berjalan sebagai root; pembekuan berlaku untuk semua identitas.
  2. Lubang tmpfs hidup dan terpakai — berkas uji tercipta di samping lima direktori kerja yang dibuat nginx sendiri saat menyala; tanpa lubang ini, mkdir client_temp pada proses startup-lah yang akan gagal lebih dulu.
  3. Layanan tetap sehat ujung ke ujung — curl menerima 200; penguncian filesystem tidak mengurangi satu pun fungsi yang memang dibutuhkan pengguna.

Status penguncian terbaca dari docker inspect, sebelum container uji dibersihkan dengan docker rm -f web-kekal:

bash
$ docker inspect --format '{{.HostConfig.ReadonlyRootfs}}' web-kekal

Output yang Dihasilkan:

text
true

Read-Only di Compose #

Pola ini menjadi konfigurasi tetap stack lewat compose.yaml — kunci read_only dan daftar tmpfs:

yaml
services:
  web:
    image: nginx:1.29
    ports:
      - "8083:80"
    read_only: true
    tmpfs:
      - /run
      - /var/cache/nginx

Bentuk daftar di atas tidak menyediakan opsi tambahan; kuota dan pengaturan lain memakai sintaks panjang volumes bertipe tmpfs:

yaml
services:
  web:
    image: nginx:1.29
    read_only: true
    volumes:
      - type: tmpfs
        target: /run
      - type: tmpfs
        target: /var/cache/nginx
        tmpfs:
          size: 16m

Mari kita telaah pemetaan kedua bentuk tersebut:

  1. read_only: true — padanan --read-only pada docker run; seluruh rootfs service ini dibekukan.
  2. Daftar tmpfs — padanan paling ringkas dari --tmpfs; setiap baris satu lubang tulis.
  3. Bentuk panjang dengan tmpfs.size — sintaks volumes bertipe tmpfs membuka opsi seperti kuota ukuran; perilaku noexec dan nosuid tetap menjadi pengaturan bawaan kedua bentuk.

Menyalakan stack uji dan membersihkannya kembali simetris: docker compose up -d lalu docker compose down — container yang dibangkitkan Compose membawa penguncian yang sama persis dengan versi CLI-nya.

Keuntungan Imutabilitas #

Apa yang sebenarnya kita beli dengan seluruh kerumitan di atas? Tiga hal. Pertama, malware kehilangan tempat menulis: sebagian besar implant butuh menjatuhkan berkas — payload, backdoor, alat eskalasi — untuk bertahan, dan kernel kini menolak setiap penulisan itu di seluruh akar filesystem; lubang tmpfs yang tersisa hidup di RAM dan hilang saat container berhenti. Kedua, restart selalu bersih: state container kembali persis ke isinya saat menyala, karena memang tidak ada state yang bisa tersimpan — kejanggalan konfigurasi yang “muncul sendiri” di production jadi mustahil secara struktural, bukan sekadar dilarang secara kebijakan. Ketiga, kepastian isi: dikombinasikan dengan pematokan image berdasarkan digest — sha256 yang kita kenal dari Chapter Image Digest — kita memegang jaminan berlapis bahwa yang berjalan hari ini adalah bytenya byte yang sama dengan yang diaudit; kombinasi read-only plus digest pinning adalah fondasi pola immutable infrastructure yang kita rangkai penuh pada Chapter Reproducible Build.

Kejujuran tetap perlu disampaikan: read-only bukan antivirus. Proses yang sudah berjalan tetap bisa berbuat banyak di dalam memori dan jaringan, dan lubang tmpfs tetap bisa dipakai menulis sementara. Yang dibeli flag ini adalah penghapusan satu kelas daya rusak yang paling sering dipakai — persistensi di disk — dengan harga satu flag dan pemetaan beberapa lubang tulis.

Kombinasi Emas #

Seluruh pelajaran section ini — identitas, capability, filesystem — bisa dirangkai menjadi satu baris uji yang layak dihafal:

bash
$ docker run --rm --read-only --user 1000:1000 --cap-drop=ALL \
    alpine:3.20 sh -c 'id && touch /uji'

Output yang Dihasilkan:

text
uid=1000 gid=1000
touch: /uji: Read-only file system

Untuk service sungguhan, satu kebutuhan tulis hampir pasti muncul lebih dulu dari yang lain: /tmp. Runtime Python menyimpan berkas sementara di sana, demikian pula JVM dan toolchain Go; pola --tmpfs /tmp karenanya menjadi default yang nyaris otomatis pada setiap container read-only, persis mekanisme tmpfs yang kita bedah pada Chapter tmpfs Mount.

Dari lima lapisan isolasi pada diagram Chapter Prinsip Keamanan Container, tiga lapisan terluar kini kita tegakkan secara sadar: identitas diturunkan, privilege dicabut sampai sisa yang terbukti dibutuhkan, dan filesystem dibekukan dengan lubang-lubang yang dipilih. Dua lapisan terdalam — seccomp dan LSM — masih bekerja dengan pengaturan bawaannya: profil default yang baik, tetapi belum kita kenal dan belum kita kendalikan. Padahal dari semua lapisan itu, keduanyalah yang tetap bekerja pada detik terburuk: ketika penyerang sudah memegang shell di dalam container. Chapter Seccomp, AppArmor, dan SELinux membuka lapisan tersebut — cara kernel menyaring syscall yang boleh lewat, profil AppArmor yang menempel pada container, dan bagaimana keduanya dikustomisasi untuk aplikasi kita.

Referensi #