Container Hak Minimal

Pengguna non-root dengan USER, sistem berkas read-only, dan mengurangi permukaan serangan container.

Daftar isi

Container yang dibuat tanpa pengaturan keamanan apa pun memberi hak lebih besar daripada yang dibutuhkan aplikasinya. Topik ini membahas tiga pengaturan hak minimal, yaitu pengguna non-root untuk proses, sistem berkas read-only, serta penurunan hak kernel container. Pengguna non-root adalah pengguna biasa tanpa hak administratif, sedangkan sistem berkas read-only adalah sistem berkas yang hanya dapat dibaca dan tidak dapat ditulis. Aplikasi catatan Python yang digunakan di sepanjang jalur menjadi contohnya, mulai dari Dockerfile yang diperbaiki hingga opsi docker run yang menyertainya. Setelah topik ini dibaca, container dapat dijalankan dengan hak sekecil mungkin tanpa mengganggu kerja aplikasinya.

Root sebagai pengguna bawaan#

Tanpa instruksi USER pada Dockerfile, daemon menjalankan proses utama container sebagai root, akun tertinggi di sistem Linux yang berhak melakukan apa saja terhadap sistem berkas dan proses. Perilaku bawaan ini berlaku pada banyak image, termasuk image dasar yang belum disetel, sehingga aplikasi catatan sederhana pun berjalan dengan hak penuh tanpa alasan yang jelas. Hak sebesar itu tidak dibutuhkan aplikasi yang hanya melayani permintaan HTTP dan membaca berkas miliknya sendiri.

Masalahnya baru terlihat ketika isolasi container gagal. Namespace dan cgroup memisahkan container dari host, tetapi pemisahan itu adalah perangkat lunak yang sama rentannya dengan kode lain. Apabila terjadi container escape, keadaan ketika proses berhasil lolos dari isolasi container dan menjangkau host, proses yang berjalan sebagai root di dalam container membawa hak root itu pula ke host. Dengan pengguna biasa, pelarian yang sama hanya menghasilkan akses terbatas, sehingga dampaknya jauh lebih kecil.

USER dan COPY –chown pada Dockerfile#

Instruksi USER menetapkan pengguna untuk seluruh instruksi yang mengikutinya dan untuk proses utama container. Tempatkan instruksi ini setelah pekerjaan yang memang membutuhkan root, misalnya memasang paket dengan apt-get atau membuat pengguna baru, dan sebelum kode aplikasi disalin. Dockerfile aplikasi catatan berikut memuat urutan yang benar.

dockerfile Dockerfile
# Image dasar: Python 3.12 varian slim berbasis Debian
FROM python:3.12-slim

# Direktori kerja untuk instruksi berikutnya dan untuk container
WORKDIR /aplikasi

# Membuat pengguna khusus aplikasi, dijalankan selagi proses build masih root
RUN useradd --create-home appuser

# Menyalin kode aplikasi sekaligus menetapkan kepemilikannya kepada appuser
COPY --chown=appuser:appuser app.py .

# Direktori data disiapkan dan diberikan kepada appuser
RUN mkdir data && chown appuser:appuser data

# Seluruh instruksi berikutnya dan proses container berjalan sebagai appuser
USER appuser

# Menonaktifkan penulisan berkas .pyc agar direktori aplikasi tetap bersih
ENV PYTHONDONTWRITEBYTECODE=1

# Perintah default saat container berjalan
CMD ["python3", "app.py"]

Tiga perubahan menonjol dibandingkan Dockerfile sebelumnya pada jalur ini. Instruksi RUN useradd --create-home appuser membuat pengguna appuser selagi proses build masih berjalan sebagai root, karena pembuatan pengguna memang membutuhkan hak itu. Instruksi COPY --chown=appuser:appuser app.py . menyalin kode sekaligus menetapkan kepemilikannya kepada appuser, sehingga tidak perlu langkah chown terpisah yang justru menambah layer baru. Terakhir, USER appuser menggeser seluruh instruksi berikutnya dan proses container keluar dari root, sedangkan ENV PYTHONDONTWRITEBYTECODE=1 mencegah Python menulis berkas .pyc ke direktori aplikasi saat berjalan. Direktori data ikut diberikan kepada appuser agar aplikasi yang kelak menulis catatan ke sana tidak terbentur izin.

Menjalankan ulang image pihak ketiga dengan –user#

Image pihak ketiga biasanya tidak dapat disetel ulang karena Dockerfilenya tidak tersedia, tetapi penggunanya tetap dapat diganti saat container dibuat. Opsi --user pada docker run mengganti UID dan GID proses tanpa membangun ulang image, sehingga image milik orang lain pun dapat dijalankan dengan hak kecil. Hak prosesnya kini mengikuti angka yang diberikan, bukan pengguna bawaan image.

bash
# tidak-dijalankan: daemon Docker tidak tersedia di sandbox
# menjalankan image pihak ketiga sebagai UID dan GID 1000
docker run --rm --user 1000:1000 image-pihak-ketiga

Opsi ini tidak membuat pengguna baru dan tidak mengubah kepemilikan berkas di dalam image. Prosesnya sekadar berjalan dengan angka UID dan GID yang diberikan, dan perintah id di dalam container menampilkannya bila perlu diverifikasi. Karena berkas aplikasi mungkin tetap milik root, aplikasi yang menulis ke direktorinya sendiri dapat gagal dengan pesan error permission denied. Pada kasus itu, direktori yang memang harus ditulis dipasang sebagai volume yang kepemilikannya cocok dengan UID tersebut. Nilai 1000 dipakai pada contoh karena menjadi UID umum bagi pengguna pertama di distribusi Linux, sedangkan id -u pada host menunjukkan nilai milik pengguna yang sedang dipakai.

Sistem berkas read-only dan tmpfs#

Opsi --read-only memasang seluruh sistem berkas container sebagai hanya-baca, sehingga proses yang sudah menguasai container sekalipun tidak dapat menaruh berkas baru. Aplikasi tetap kerap menulis ke /tmp, sehingga direktori itu membutuhkan pengecualian. Pengecualian itu disediakan tmpfs, yaitu sistem berkas sementara yang tersimpan di memori dan hilang saat container berhenti. Pada docker run, tmpfs dipasang melalui opsi --tmpfs. Kedua opsi itu selalu dipasangkan pada aplikasi yang masih membutuhkan direktori sementara.

bash
# tidak-dijalankan: daemon Docker tidak tersedia di sandbox
# membangun image aplikasi catatan dengan pengguna non-root
docker build -t aplikasi-catatan .
bash
# tidak-dijalankan: daemon Docker tidak tersedia di sandbox
# menjalankan container dengan sistem berkas read-only dan tmpfs untuk /tmp
docker run --rm --read-only --tmpfs /tmp -p 8000:8000 aplikasi-catatan

Aplikasi catatan cocok dengan pola ini karena tidak menulis apa pun ke sistem berkas saat berjalan, melainkan menyimpan datanya di memori. ENV PYTHONDONTWRITEBYTECODE=1 pada Dockerfile mencegah satu penulisan tersembunyi, yaitu berkas .pyc yang biasa dibuat Python. Untuk direktori data yang memang harus bertahan, volume dipasang sebagaimana dibahas pada modul Data Persisten, sedangkan --tmpfs hanya untuk direktori sementara yang isinya boleh hilang.

Mengurangi permukaan serangan#

Pengaturan hak minimal bekerja secara berlapis, dan setiap lapisan menutup celah yang ditinggalkan lapisan lainnya. Tabel berikut merangkum lapisan yang telah dibahas beserta pengaturannya.

LapisanPengaturanEfek
DockerfileUSER appuserproses container berjalan sebagai pengguna aplikasi
DockerfileCOPY --chown=appuser:appuserberkas aplikasi berpindah kepemilikan sejak build
docker run--user 1000:1000pengguna proses diganti tanpa mengubah image
docker run--read-only dan --tmpfssistem berkas utama terkunci, /tmp tetap dapat ditulis
docker run--cap-drop=ALLhak kernel dilepas selain yang dikembalikan lewat --cap-add

Lapisan terakhir berada pada kernel. Opsi --cap-drop=ALL melepaskan seluruh kapabilitas (capability), yaitu satuan hak istimewa kernel yang diberikan kepada container satu per satu, sedangkan hak yang benar-benar diperlukan dikembalikan melalui --cap-add. Sebagian besar aplikasi biasa tidak membutuhkan kapabilitas apa pun, sehingga pelepasan seluruhnya menjadi pilihan bawaan yang baik. Lapisan paling luar adalah rootless mode, yakni menjalankan daemon Docker itu sendiri sebagai pengguna biasa pada host, sehingga kebocoran pada daemon pun tidak langsung memberikan hak root. Seluruh lapisan itu saling melengkapi, dan satu lapisan saja tidak pernah cukup untuk menyebut container aman.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul saat prinsip hak minimal dipraktikkan.

  • Menempatkan USER setelah CMD. USER berlaku bagi instruksi berikutnya dan bagi container, sehingga penempatan di baris paling akhir tidak mengubah pengguna proses sama sekali. Perbaikannya, tulis USER sebelum CMD.
  • Menyalin berkas tanpa --chown. Berkas menjadi milik root, lalu aplikasi gagal membaca atau menulisnya dengan pesan error izin. Perbaikannya, tambahkan COPY --chown=appuser:appuser atau tetapkan kepemilikan dengan RUN chown.
  • Memakai --read-only tanpa menyiapkan direktori tulis. Aplikasi yang menulis /tmp atau tempat lain langsung berhenti dengan error. Perbaikannya, pasang --tmpfs untuk direktori sementara dan volume untuk data yang harus bertahan.
  • Mengisi --user tanpa mencocokkan kepemilikan berkas. Aplikasi yang membaca berkas milik UID lain tetap gagal walau prosesnya sudah bukan root. Perbaikannya, cocokkan UID dan GID dengan kepemilikan berkas yang diperlukan aplikasi.

Ringkasan#

  • Dockerfile tanpa USER menjalankan proses container sebagai root, sehingga container escape membawa hak root tersebut ke host.
  • USER appuser disertai COPY --chown=appuser:appuser memindahkan proses dan kepemilikan berkas kepada pengguna aplikasi sejak build.
  • Image pihak ketiga dijalankan ulang dengan --user, sedangkan sistem berkas dikunci dengan --read-only dan dilonggarkan hanya pada /tmp melalui --tmpfs.
  • --cap-drop=ALL melepaskan hak kernel container dan melengkapi pola hak minimal dari sisi kernel.

Referensi#