Identitas container kini diturunkan, tetapi rumus Chapter Prinsip Keamanan Container masih menyimpan suku yang lebih besar: sekalipun berjalan sebagai user biasa — atau justru masih sebagai root — kekuatan yang melekat pada proses itu sendiri belum kita sentuh. Sejak kernel Linux 2.2 dirilis akhir 1990-an, privilege root yang dulu monolitis dipecah menjadi unit-unit kecil bernama capability, dan Docker sejak awal memanfaatkan pecahan itu: container bawaan hanya menerima dua belas dari sekitar tiga puluh delapan capability yang dimiliki root host. Chapter ini membedah peta dua belas capability itu, membuktikan mana yang absen lewat perintah yang gagal, lalu mematangkan pola produksi yang paling dianjurkan: cabut semuanya dengan --cap-drop=ALL, lalu tambahkan kembali hanya yang benar-benar dibutuhkan aplikasi — satu per satu, dituntun oleh pesan galat.
Root yang Dipecah Menjadi Capabilities #
Sistem Unix klasik mengenal dua kasta: root yang boleh segalanya, dan user biasa yang dibatasi izin berkas. Model dua kasta itu kasar untuk kebutuhan modern — daemon pencatat log misalnya hanya butuh hak mengikat port rendah, bukan hak memformat disk. Kernel Linux 2.2 karenanya memecah kekuatan root menjadi unit-unit bernama capability: CHOWN untuk mengubah kepemilikan berkas, NET_BIND_SERVICE untuk mengikat port di bawah 1024, SYS_TIME untuk mengubah jam sistem, dan seterusnya hingga sekitar tiga puluh delapan unit pada kernel masa kini. Sebuah proses membawa kumpulan capability dalam atributnya, dan kernel memeriksa capability itu setiap kali syscall sensitif dipanggil — terlepas dari apakah proses berjalan sebagai UID 0 atau bukan.
Pemecahan inilah yang menjelaskan satu kejutan dari chapter sebelumnya: container non-root yang kebetulan masih bisa mengikat port 80, karena NET_BIND_SERVICE melekat pada proses container bawaan apa pun identitas user-nya. Dua sumbu — identitas (UID) dan capability — bekerja independen, dan keamanan yang matang mengatur keduanya.
Dua Belas Capability Bawaan Container #
Docker memberikan dua belas capability ini kepada setiap container bawaan:
| Capability | Daya yang diberikan |
|---|---|
CHOWN | mengubah kepemilikan berkas dengan chown |
DAC_OVERRIDE | melewati pemeriksaan izin baca-tulis-eksekusi berkas |
FOWNER | mengubah izin berkas yang bukan milik proses |
FSETID | menyetel bit setuid dan setgid pada berkas |
KILL | mengirim sinyal ke proses milik user lain |
MKNOD | membuat berkas device khusus dengan mknod |
NET_BIND_SERVICE | mengikat port di bawah 1024 |
SETFCAP | menyetel capability pada berkas |
SETGID | mengubah GID proses |
SETPCAP | menambah capability dalam batas yang diizinkan |
SETUID | mengubah UID proses |
AUDIT_WRITE | menulis catatan audit ke kernel |
Yang Tidak Ada di Daftar #
Jauh lebih penting dari yang ada adalah yang tidak ada. Root host memegang NET_ADMIN untuk menata antarmuka jaringan dan routing; root container tidak. Root host memegang NET_RAW untuk membuat socket paket mentah — alat kerja tcpdump dan tool jaringan tingkat rendah lainnya; root container tidak. Root host memegang SYS_ADMIN yang membukakan mount, pembuatan namespace, dan puluhan operasi administratif lain; root container tidak. Root host bisa mengubah jam sistem lewat SYS_TIME dan memuat modul kernel lewat SYS_MODULE; root container tidak bisa keduanya. Kesimpulannya tegas: root di dalam container tetap lebih lemah daripada root di host — bukan karena nama root-nya berbeda, melainkan karena daftar capability yang dibawanya jauh lebih pendek.
Mencoba Capability yang Absen #
Teori di atas bisa dibuktikan dengan satu perintah jam. Container ubuntu mencoba mengubah tanggal sistemnya:
$ docker run --rm ubuntu:24.04 date -s "2026-01-01"Output yang Dihasilkan:
date: cannot set date: Operation not permittedMari kita telaah penolakan tersebut:
Operation not permittedadalah kode EPERM dari kernel — kernel menolak syscall pengubah jam karenaSYS_TIMEtidak dibawa proses ini; Docker memang tidak pernah memberikannya ke container bawaan.- Jam adalah milik kernel, dan kernelnya satu — container tidak punya kernel sendiri, sehingga tidak ada “jam container” yang bisa digeser terpisah dari host; berbeda dari mesin virtual yang membawa kernel dan jam sendiri di dalam guest.
- Pesan galat tidak pernah menyebut capability apa yang absen — diagnosa semacam ini menuntut peta seperti tabel di atas; mengenali pola “EPERM pada operasi administratif” adalah keterampilan inti section ini.
Mencabut yang Ada #
Sisi sebaliknya juga layak dibuktikan: capability yang memang dibawa container bisa kita cabut dengan flag --cap-drop. Pasangan berikut membandingkan chown sebelum dan sesudah seluruh capability dicabut:
$ docker run --rm ubuntu:24.04 sh -c "touch /uji && chown 1000:1000 /uji && echo chown-sukses"Output yang Dihasilkan:
chown-sukses$ docker run --rm --cap-drop=ALL ubuntu:24.04 sh -c "touch /uji && chown 1000:1000 /uji && echo chown-sukses"Output yang Dihasilkan:
chown: changing ownership of '/uji': Operation not permittedMari kita telaah pasangan tersebut:
chownberhasil pada container bawaan —CHOWNada di daftar dua belas; proses root container berhak mengubah kepemilikan berkasnya.--cap-drop=ALLmelucuti seluruhnya — dengan daftar capability kosong, kernel menolakchownmeski proses berjalan sebagai UID 0; capability diperiksa terpisah dari identitas user.touchtetap berhasil pada keduanya — membuat berkas di direktori/yang dimiliki root hanya butuh hak pemilik direktori, bukan capability tertentu; pencabutan capability tidak melumpuhkan operasi berkas yang wajar.
Pola Produksi: Buang Semua, Tambah Seperlunya #
Dua belas capability bawaan adalah tebakan kompromi yang enak untuk mayoritas aplikasi — tetapi kebijakan keamanan yang serius tidak bertumpu pada tebakan. Pola yang dianjurkan bekerja seperti daftar izin (allowlist): mulai dari nol hak, lalu tambahkan hanya yang terbukti dibutuhkan. Kita peragakan siklusnya dengan server berkas kecil milik Python, dari titik paling ketat:
$ docker run -d --name berkas-publik --cap-drop=ALL -p 8084:8080 \
python:3.12-slim python -m http.server 8080Output yang Dihasilkan:
9a3c7e1b5d2f4a6c8e0b2d9f1a4c6e8b3d5f7a9c1e3b5d7f2a4c6e8b0d1f3a5c$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8084Output yang Dihasilkan:
200Aplikasi yang hanya membaca berkas dan mendengarkan port tinggi tidak merasakan satu capability pun hilang. Sekarang kita pindahkan layanan yang sama ke port 80 — port rendah yang dilindungi kernel:
$ docker rm -f berkas-publik
$ docker run -d --name berkas-publik --cap-drop=ALL -p 8084:80 \
python:3.12-slim python -m http.server 80Output yang Dihasilkan:
8c2e9a4f1b7d3c6e5a8f0b2d4c6e8a1f3b5d7c9e0a2f4b6d8c1e3a5f7b9d0c2e$ docker ps -a --filter name=berkas-publikOutput yang Dihasilkan:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8c2e9a4f1b7d python:3.12-slim "python -m http.serve…" 9 seconds ago Exited (1) 4 seconds ago berkas-publik$ docker logs berkas-publikOutput yang Dihasilkan:
Traceback (most recent call last):
File "<frozen runpy>", line 198, in _run_module_as_main
...
File "/usr/local/lib/python3.12/socketserver.py", line 305, in server_bind
socket.bind(self.server_address)
OSError: [Errno 13] Permission deniedLog error inilah kompas kita. Kita tambahkan satu capability yang ditunjuknya, lalu ulangi:
$ docker rm -f berkas-publik
$ docker run -d --name berkas-publik --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
-p 8084:80 python:3.12-slim python -m http.server 80Output yang Dihasilkan:
6f1d8a3c9b2e5f7a0d4c6e8b1f3a5c7e9d0b2f4a6c8e0d2b5f7a3c1e9d4b6f8a$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8084Output yang Dihasilkan:
200Mari kita telaah siklus tiga langkah di atas:
- Mulai dari titik paling ketat —
--cap-drop=ALLpada langkah pertama; aplikasi yang hanya butuh berkas dan port tinggi langsung terbukti hidup tanpa satu capability pun. - Baca galat, jangan menebak — traceback menunjuk
OSErrorerrno 13 tepat saatsocket.bindpada port 80; galat izin saat mengikat port rendah adalah tanda tanganNET_BIND_SERVICEyang absen, dan pencarian singkat atas pesan itu mengarah ke capability yang sama. - Tambahkan persis satu capability yang ditunjuk, lalu ulangi — container kini membawa satu capability, bukan dua belas; setiap hak yang melekat pada proses adalah hak yang bisa dipertanggungjawabkan.
Keadaan akhir container bisa diverifikasi dari luar tanpa membuka shell. docker inspect dengan format template membaca dua field konfigurasi host:
$ docker inspect --format '{{.HostConfig.CapDrop}} {{.HostConfig.CapAdd}}' berkas-publikOutput yang Dihasilkan:
[ALL] [NET_BIND_SERVICE]Output itu adalah dua slice Go yang dicetak template: [ALL] — seluruh capability dicabut — dan [NET_BIND_SERVICE] — satu-satunya yang ditambahkan kembali. Beres untuk container latihan; jangan lupa membersihkannya dengan docker rm -f berkas-publik.
Use Case Nyata dari Lapangan #
Dua kasus nyata menunjukkan kisarannya. Pertama, nginx klasik: master process-nya berjalan sebagai root dan mendengarkan port 80, sehingga butuh NET_BIND_SERVICE; ia juga menurunkan worker ke UID 101 — pekerjaan yang menuntut SETUID dan SETGID; dan entry point-nya membereskan direktori cache saat menyala, yang menuntut CHOWN, DAC_OVERRIDE, dan FOWNER. Daftar yang “seperperlunya” untuk nginx sungguhan karenanya cukup panjang — dan itu kabar jujur yang perlu disampaikan: pola drop-ALL menuntut kesabaran untuk image yang dirancang era kebebasan root. Dua jalan pragmatis menyambut: pakai varian nginx-unprivileged yang mendengarkan 8080 sejak lahir sehingga nyaris tak butuh apa pun, atau biarkan container mendengarkan 8080 dan terbitkan -p 80:8080 seperti pola favorit Chapter Menjalankan Container Non-Root.
Kedua, penyadapan lalu lintas dengan tcpdump — image netshoot kerap kita pakai untuk diagnosa jaringan. Tanpa capability tambahan, tcpdump langsung tersandung:
$ docker run --rm nicolaka/netshoot tcpdump -c 1 -i eth0Output yang Dihasilkan:
tcpdump: socket: Operation not permittedSocket paket mentah menuntut NET_RAW, dan mode promiscuous menuntut NET_ADMIN — dua-duanya di luar daftar bawaan. Ditambah --net=host supaya melihat interface host dan bukan sekadar veth container, kombinasi lengkapnya berbunyi:
$ docker run --rm --net=host --cap-add=NET_RAW --cap-add=NET_ADMIN \
nicolaka/netshoot tcpdump -c 2 -i anyOutput yang Dihasilkan:
tcpdump: data link type PKTAP
tcpdump: verbose output suppressed, use -v[v...] for full protocol decode
listening on any, link-type PKTAP (Linux Cooked v1), snapshot length 262144 bytes
...
2 packets captured
2 packets received by filter
0 packets dropped by kernelKasus ini sekaligus contoh sehat pemberian capability: dua hak spesifik diberikan untuk satu pekerjaan diagnosa, container sekali pakai, lalu lenyap — bukan memberikan --privileged hanya demi satu tool.
Capability yang Harus Dicurigai #
Menambah capability kadang memang perlu; lima nama berikut layak melebihi sekadar dicurigai:
| Capability | Daya yang dibuka | Kenapa berbahaya |
|---|---|---|
SYS_ADMIN | mount, pembuatan namespace, dan puluhan operasi administratif lain | setara root di host — pintu utama breakout |
NET_ADMIN | menata antarmuka, routing, dan aturan firewall | bisa membongkar jaringan host dan container tetangga |
SYS_MODULE | memuat modul kernel ke dalam kernel yang berjalan | modul jahat berarti kode menempel di dalam kernel host |
SYS_PTRACE | mengintip dan menyuntik proses lain | bisa membaca memori proses host dan mencuri kredensialnya |
NET_RAW | membuat socket paket mentah | penyadapan dan pemalsuan lalu lintas jaringan |
Capabilities di Compose #
Seluruh flag docker run ini punya padanan first-class di compose.yaml, sehingga pola drop-ALL bisa menjadi konfigurasi tetap stack dan ikut versioned bersama kode:
services:
berkas-publik:
image: python:3.12-slim
command: python -m http.server 80
ports:
- "8084:80"
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICEMari kita telaah pemetaannya:
cap_drop: - ALL— padanan--cap-drop=ALL; daftar di Compose juga menerima nama capability spesifik bila hanya sebagian yang dicabut.cap_add: - NET_BIND_SERVICE— padanan--cap-add; daftar ini hanya menambahkan kembali apa yang memang dibutuhkan service bersangkutan.commandmenimpa CMD image — baris ini menyetel aplikasi ke port 80 di dalam container, danportsmenerbitkannya sebagai 8084 di host; pola port tinggi-versus-pemetaan dari chapter sebelumnya tetap berlaku berdampingan dengan capability.
Kita kini mengendalikan dua sumbu sekaligus: identitas proses dari chapter sebelumnya, dan daftar privilege yang dibawanya di chapter ini. Tetapi ada satu bendera yang sengaja kita simpan terakhir — bendera yang membalik seluruh kerja keras ini dalam satu kata: --privileged memberikan semua capability sekaligus, membukakan seluruh device host, dan mematikan seccomp beserta LSM dalam satu tarikan napas. Chapter Privileged Container membongkar apa saja yang sebenarnya dilepasnya, membuktikan bahayanya lewat disk host yang tiba-tiba terlihat, lalu memetakan lima alternatif yang lebih aman untuk setiap alasan klasik pemakaian --privileged.
Referensi #
- Docker security — penjelasan resmi capabilities bawaan container dan risiko menambahnya.
- docker container run — CLI Reference
— referensi flag
--cap-adddan--cap-drop. - Compose file reference
— kunci
cap_adddancap_droppada service Compose.