Keamanan Runtime

Keamanan Runtime

Bitnesia Sep 12, 2026 10 EN

Image yang sudah bersih dari vulnerability tetap bisa disalahgunakan kalau container yang menjalankannya diberi keleluasaan penuh terhadap kernel host, resource sistem, jaringan, maupun kredensial yang ditempelkan sembarangan. Keamanan runtime mengatur apa yang boleh dan tidak boleh dilakukan sebuah container selagi ia berjalan, terlepas dari seberapa aman image asalnya. Sysadmin/DevOps Engineer yang mengelola container di production perlu memasang beberapa lapis kontrol sekaligus: modul keamanan kernel seperti AppArmor dan SELinux untuk membatasi akses ke sistem file dan syscall, batasan resource supaya satu container nakal tidak menghabiskan memori atau CPU seluruh host, isolasi jaringan supaya container yang satu tidak bisa sembarangan menjangkau container lain, dan pengelolaan secret yang tidak membocorkan credential lewat environment variable atau image layer. Bab ini membahas keempat lapis kontrol tersebut secara praktis.

24.1 AppArmor dan SELinux

AppArmor dan SELinux adalah dua modul keamanan kernel Linux (Linux Security Module/LSM) yang membatasi apa saja yang boleh diakses sebuah proses, termasuk proses di dalam container, jauh melampaui batasan permission user biasa. Keduanya bekerja dengan prinsip serupa tapi punya cara konfigurasi yang berbeda, dan distro Linux biasanya sudah menentukan salah satunya sebagai default.

24.1.1 AppArmor Profile Default dan Kustom

AppArmor aktif secara default di distro seperti Ubuntu dan Debian, dan Docker Engine otomatis menerapkan profile bernama docker-default ke setiap container yang dijalankan tanpa perlu konfigurasi tambahan apa pun. Menurut dokumentasi resmi Docker, profile docker-default ini bersifat moderat, cukup ketat untuk memblokir banyak aksi berbahaya tapi tetap longgar supaya kompatibel dengan mayoritas aplikasi umum. Verifikasi bahwa AppArmor benar-benar aktif dan profile default terpasang lewat perintah berikut.

docker info --format '{{.SecurityOptions}}'

Output yang memuat name=apparmor menandakan AppArmor aktif pada host tersebut. Profile default bisa dinyatakan eksplisit atau digantikan dengan profile kustom lewat opsi --security-opt saat docker run.

docker run --rm -it --security-opt apparmor=docker-default alpine sh

Untuk kebutuhan yang lebih spesifik, misalnya aplikasi yang perlu dibatasi lebih ketat dari profile default, Sysadmin/DevOps Engineer bisa menulis profile AppArmor kustom sebagai file teks, memuatnya ke kernel lewat apparmor_parser, lalu menerapkannya ke container tertentu.

sudo apparmor_parser -r -W /etc/apparmor.d/nginx-restricted
docker run --rm -d --security-opt apparmor=nginx-restricted nginx:1.27

Sebaliknya, menonaktifkan AppArmor lewat --security-opt apparmor=unconfined membuat container berjalan tanpa batasan LSM sama sekali. Opsi ini kadang dibutuhkan untuk debugging atau untuk aplikasi tertentu yang memang butuh akses lebih luas, tapi attacker yang berhasil mengeksploitasi vulnerability di dalam container unconfined punya ruang gerak jauh lebih besar dibanding container yang tetap memakai profile default.

24.1.2 SELinux Label dan Context

SELinux jadi default di distro turunan Red Hat seperti RHEL, Fedora, dan CentOS, dan bekerja dengan menempelkan label keamanan ke setiap proses dan resource sistem, lalu memutuskan izin akses berdasarkan kecocokan label tersebut alih-alih hanya permission Unix biasa. Ketika SELinux aktif dalam mode enforcing, proses di dalam container otomatis mendapat label tipe container_t, sementara file yang di-bind mount dari host tetap membawa label aslinya kecuali di-relabel secara eksplisit.

getenforce

Masalah paling umum yang ditemui Sysadmin/DevOps Engineer di lapangan saat memakai bind mount pada host dengan SELinux aktif adalah permission denied meski permission Unix pada file sudah benar, karena label SELinux pada direktori host tidak cocok dengan label yang diharapkan proses di dalam container. Solusinya memakai suffix :z atau :Z pada opsi volume saat docker run supaya Docker merelabel direktori tersebut secara otomatis.

docker run --rm -d -v /opt/app/config:/config:z nginx:1.27

Suffix :z (huruf kecil) memberi label shared yang boleh diakses banyak container sekaligus, sedangkan :Z (huruf besar) memberi label private yang membatasi akses hanya untuk container tersebut lewat kategori MCS (Multi-Category Security) yang unik. Untuk kebutuhan yang lebih granular, label SELinux pada container juga bisa diatur langsung lewat --security-opt label.

docker run --rm -it --security-opt label=level:s0:c100,c200 alpine sh

Sama seperti AppArmor, SELinux juga bisa dinonaktifkan per container lewat --security-opt label=disable, tapi keputusan ini sebaiknya jadi pengecualian yang didokumentasikan alasannya, bukan kebiasaan default untuk menghindari troubleshooting label.

24.1.3 Memilih Modul Sesuai Distro Host

AppArmor dan SELinux tidak berjalan bersamaan pada satu host Linux; sistem hanya memakai salah satunya sesuai dukungan bawaan distro yang dipakai. Sysadmin/DevOps Engineer tidak perlu memilih di antara keduanya secara manual, karena pilihan ini sudah ditentukan oleh distro host: Ubuntu dan Debian datang dengan AppArmor, sementara RHEL, Fedora, dan CentOS datang dengan SELinux. Yang perlu dipastikan adalah modul keamanan bawaan distro tersebut benar-benar aktif dan tidak sengaja dinonaktifkan demi kemudahan, karena menonaktifkan LSM sama saja menghilangkan satu lapis pertahanan penting terhadap container escape, terutama saat vulnerability baru pada container runtime ditemukan sebelum sempat ditambal.

24.2 Membatasi Resource Container

Container yang berjalan tanpa batasan resource bisa menghabiskan seluruh memori atau CPU host, baik karena bug pada aplikasi seperti memory leak, maupun karena ulah attacker yang sengaja memicu denial of service dari dalam container yang berhasil dikuasainya. Docker memanfaatkan cgroups pada kernel Linux untuk membatasi resource ini per container.

24.2.1 Memory Limit dan Perilaku OOM Killer

Opsi --memory (atau -m) pada docker run menetapkan batas maksimum memori yang boleh dipakai sebuah container. Kalau container mencoba memakai memori melebihi batas ini, OOM killer pada kernel Linux akan menghentikan paksa proses di dalam container tersebut.

docker run -d --memory=512m --memory-swap=512m --name payment-api payment-service:1.4.2

Menetapkan --memory-swap dengan nilai yang sama seperti --memory pada contoh di atas mematikan penggunaan swap tambahan, sehingga total memori yang bisa dipakai container benar-benar terbatas pada nilai --memory. Kalau --memory-swap tidak diset, Docker secara default mengizinkan swap tambahan sebesar nilai --memory yang sama, sehingga total memori plus swap bisa mencapai dua kali lipat dari batas yang terlihat.

Perilaku OOM killer bisa disesuaikan lewat dua opsi tambahan. --oom-score-adj mengatur prioritas container tersebut untuk dipilih atau dihindari OOM killer relatif terhadap proses lain di host, dengan rentang nilai -1000 (hampir tidak pernah dipilih) sampai 1000 (paling diprioritaskan untuk dihentikan). Sementara --oom-kill-disable mencegah OOM killer mematikan container sama sekali, tapi opsi ini berisiko tinggi karena container yang kehabisan memori tanpa bisa dihentikan bisa membuat seluruh host jadi tidak responsif; pakai opsi ini hanya untuk container kritis yang sudah dipasangi --memory dengan batas yang benar-benar aman.

docker stats payment-api --no-stream

Perintah docker stats di atas memperlihatkan pemakaian memori dan CPU aktual container dibanding batas yang sudah ditetapkan, berguna untuk memverifikasi apakah nilai --memory yang dipasang sudah realistis atau justru terlalu ketat sampai memicu OOM killer padahal aplikasi masih berjalan normal.

24.2.2 CPU Limit

Berbeda dari memory limit yang bisa berujung container dihentikan paksa, CPU limit bekerja dengan cara throttling: container yang mencapai batas CPU-nya tidak dimatikan, tapi cuma diperlambat lewat mekanisme Completely Fair Scheduler (CFS) pada kernel Linux. Opsi --cpus menetapkan jumlah core CPU maksimum yang boleh dipakai container, dan nilainya boleh berupa pecahan.

docker run -d --cpus=1.5 --name payment-api payment-service:1.4.2

Perintah di atas membatasi container payment-api memakai maksimum 1,5 core CPU meski host punya lebih banyak core yang tersedia. Untuk kebutuhan prioritas relatif antar container alih-alih batas absolut, opsi --cpu-shares menentukan bobot alokasi CPU saat terjadi kontensi; container dengan --cpu-shares lebih tinggi mendapat porsi CPU lebih besar dibanding container lain saat CPU host sedang penuh, tapi tidak membatasi apa pun kalau CPU sedang tidak dipakai container lain.

docker run -d --cpu-shares=512 --name batch-worker worker-service:2.1.0

24.2.3 PID Limit dan Ulimits

Selain memori dan CPU, jumlah proses yang boleh berjalan di dalam satu container juga perlu dibatasi, karena aplikasi yang membuat proses atau thread tanpa batas, baik karena bug fork bomb maupun karena dieksploitasi attacker, bisa menghabiskan slot proses di seluruh host dan mengganggu container lain. Opsi --pids-limit menetapkan jumlah maksimum proses dan thread yang boleh berjalan di dalam cgroup container.

docker run -d --pids-limit=100 --name payment-api payment-service:1.4.2

Selain --pids-limit, opsi --ulimit mengatur batasan resource level proses seperti jumlah file descriptor terbuka (nofile) atau ukuran maksimum file yang bisa ditulis (fsize), sama seperti perintah ulimit pada shell Linux biasa tapi diterapkan khusus ke proses di dalam container.

docker run -d --ulimit nofile=1024:2048 --name payment-api payment-service:1.4.2

Di lapangan, batasan resource seperti ini paling efektif dipasang sebagai standar baku di seluruh cluster lewat docker-compose.yml atau template deployment, bukan ditambahkan satu per satu secara manual setiap kali container baru dijalankan, supaya tidak ada container yang lolos tanpa batasan resource sama sekali.

24.3 Network Policies dan Isolasi Jaringan

Container yang berada di jaringan Docker yang sama secara default bisa saling terhubung tanpa batasan, sebuah kondisi yang nyaman untuk development tapi berisiko di production kalau salah satu container berhasil dikuasai attacker dan dipakai sebagai batu loncatan untuk menyerang container lain. Docker tidak punya objek network policy deklaratif seperti pada Kubernetes, tapi menyediakan beberapa mekanisme setara lewat kombinasi custom network, opsi isolasi bawaan, dan aturan firewall.

24.3.1 Isolasi lewat Custom Bridge Network

Praktik paling dasar untuk membatasi jangkauan komunikasi antar container adalah memisahkan service ke dalam beberapa custom bridge network sesuai kebutuhan komunikasinya, alih-alih menaruh semua container ke satu network yang sama. Container yang tidak perlu saling bicara ditempatkan di network yang berbeda sehingga secara default memang tidak bisa saling menjangkau.

docker network create frontend-net
docker network create backend-net

docker run -d --network frontend-net --name web nginx:1.27
docker run -d --network backend-net --name db postgres:16
docker run -d --network frontend-net --network-alias api --name api payment-service:1.4.2
docker network connect backend-net api

Pada contoh di atas, container web hanya terhubung ke frontend-net dan tidak bisa menjangkau db sama sekali, sementara api sengaja dihubungkan ke kedua network supaya bisa diakses dari frontend-net sekaligus mengakses db di backend-net. Verifikasi isolasi ini dengan mencoba ping dari container web ke container db; koneksi yang gagal menandakan isolasi network sudah berjalan sesuai rencana.

docker exec web ping -c 2 db

Untuk network yang memang tidak boleh punya akses keluar sama sekali, misalnya network yang cuma dipakai database internal, opsi --internal saat membuat network mencegah container di dalamnya mendapat default gateway ke luar host.

docker network create --internal backend-net

Container yang terhubung ke network --internal tidak bisa melakukan koneksi keluar ke internet maupun ke network Docker lain di luar dirinya, cocok untuk database atau service internal yang memang seharusnya cuma dijangkau dari dalam cluster yang sama.

24.3.2 Menonaktifkan Inter-Container Communication

Selain memisahkan network, Docker juga menyediakan opsi Inter-Container Communication (ICC) yang mengontrol apakah container dalam satu network bridge yang sama boleh saling berkomunikasi langsung lewat IP container. Menonaktifkan ICC pada default bridge network dilakukan lewat konfigurasi daemon di /etc/docker/daemon.json.

{
  "icc": false
}

Setelah mengubah daemon.json, restart Docker daemon supaya konfigurasi diterapkan.

sudo systemctl restart docker

Pengaturan icc pada daemon.json ini hanya berlaku untuk default bridge network. Untuk custom network, opsi setara diberikan lewat driver option saat network dibuat.

docker network create -o com.docker.network.bridge.enable_icc=false isolated-net

ICC yang nonaktif membuat Docker menambahkan aturan iptables yang memblokir seluruh trafik langsung antar container pada network tersebut, dan container yang tetap perlu saling berkomunikasi harus dihubungkan secara eksplisit lewat --link pada bridge lama atau memakai custom network terpisah seperti dibahas pada bagian sebelumnya. Menonaktifkan ICC secara default lalu membuka komunikasi hanya untuk pasangan container yang benar-benar butuh, lebih aman dibanding membiarkan seluruh container saling terbuka lalu berharap tidak ada yang disalahgunakan.

24.3.3 Firewall lewat DOCKER-USER Chain

Docker memanipulasi aturan iptables secara otomatis untuk mengatur port mapping dan forwarding container, dan manipulasi ini bisa menimpa aturan firewall kustom yang ditambahkan Sysadmin/DevOps Engineer langsung ke chain FORWARD biasa. Menurut dokumentasi resmi Docker, chain DOCKER-USER disediakan khusus supaya aturan firewall kustom tetap bertahan meski Docker daemon di-restart, karena chain ini diproses lebih dulu sebelum chain DOCKER dan DOCKER-FORWARD milik Docker sendiri.

sudo iptables -I DOCKER-USER -s 203.0.113.0/24 -j ACCEPT
sudo iptables -I DOCKER-USER -j DROP

Contoh di atas hanya mengizinkan trafik dari blok IP 203.0.113.0/24 menuju seluruh container, lalu menolak trafik lain yang tidak cocok dengan aturan sebelumnya. Urutan aturan pada DOCKER-USER penting karena iptables memproses aturan dari atas ke bawah dan berhenti begitu ada yang cocok, jadi aturan ACCEPT yang lebih spesifik harus ditempatkan sebelum aturan DROP yang bersifat umum. Perlu diperhatikan juga bahwa paket yang sampai ke chain DOCKER-USER sudah melewati proses Destination NAT milik Docker, sehingga aturan yang ditulis di sini mencocokkan alamat IP internal container, bukan alamat host asal sebelum di-NAT; uji aturan firewall di lingkungan staging dulu sebelum diterapkan ke production, karena kesalahan urutan aturan bisa memblokir trafik yang seharusnya diizinkan atau sebaliknya membuka akses yang seharusnya ditutup.

24.4 Secrets Management

Credential seperti password database, API key, atau private key butuh perlakuan khusus dibanding konfigurasi biasa, karena kebocoran secret bisa langsung membuka akses attacker ke sistem lain di luar container itu sendiri. Bagian ini membahas kenapa cara-cara umum mengirim secret ke container justru berisiko, dan mekanisme yang lebih aman untuk menggantikannya.

24.4.1 Risiko Environment Variable untuk Secret

Meneruskan secret lewat environment variable memakai opsi -e pada docker run atau bagian environment pada docker-compose.yml adalah pola yang sangat umum ditemukan tapi sebenarnya menyimpan beberapa risiko. Environment variable sebuah proses bisa dibaca lewat docker inspect oleh siapa pun yang punya akses ke Docker daemon, ikut muncul di /proc/<pid>/environ yang bisa diakses proses lain dengan privilege cukup di dalam container yang sama, dan sering tanpa sengaja ikut tercetak ke log aplikasi saat proses melakukan dump environment untuk keperluan debugging.

docker inspect --format '{{.Config.Env}}' payment-api

Perintah di atas memperlihatkan bagaimana mudahnya siapa pun dengan akses docker inspect membaca seluruh environment variable container, termasuk secret yang tanpa sadar diteruskan lewat opsi -e. Alternatif yang lebih aman adalah meneruskan secret sebagai file yang di-mount ke dalam container, karena file jauh lebih mudah dibatasi permission-nya dan tidak ikut terekspos lewat docker inspect.

24.4.2 Docker Compose Secrets

Docker Compose secrets menyediakan cara standar mengelola secret berbasis file lewat bagian secrets pada level teratas docker-compose.yml. Menurut dokumentasi resmi Compose Specification, deklarasi secrets ini tetap bisa dipakai pada Compose biasa tanpa Swarm; bedanya, di luar Swarm, secret berfungsi sebagai bind mount file biasa dengan path dan permission yang sudah distandarkan, bukan secret terenkripsi yang dikelola cluster seperti pada Swarm.

services:
  payment-api:
    image: payment-service:1.4.2
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Konfigurasi di atas membuat isi file db_password.txt tersedia di dalam container pada path /run/secrets/db_password, dan aplikasi tinggal membaca isi file tersebut alih-alih mengandalkan environment variable.

DB_PASSWORD=$(cat /run/secrets/db_password)

Pastikan file secret seperti db_password.txt tidak pernah ikut ter-commit ke repository git; tambahkan ke .gitignore dan distribusikan file tersebut lewat jalur terpisah yang lebih aman, misalnya lewat secret manager eksternal atau proses deployment yang sudah terenkripsi. Verifikasi secret benar-benar terpasang lewat perintah berikut, tanpa perlu membuka isi filenya di terminal.

docker compose exec payment-api ls -l /run/secrets/

24.4.3 Docker Secrets di Swarm Mode

Saat aplikasi dijalankan lewat Swarm mode, mekanisme docker secret memberi jaminan lebih kuat dibanding secret berbasis file pada Compose biasa: isi secret disimpan terenkripsi pada Raft log milik Swarm manager, didistribusikan hanya ke node yang menjalankan service terkait, dan dipasang ke container sebagai tmpfs di memori sehingga tidak pernah tertulis ke disk.

printf "SuperSecretPassword123" | docker secret create db_password -
docker service create \
  --name payment-api \
  --secret db_password \
  payment-service:1.4.2

Sama seperti pada Compose, secret yang dibuat lewat docker secret create tersedia di dalam container pada path /run/secrets/db_password. Kalau isi secret perlu diperbarui, misalnya untuk rotasi password database secara berkala, secret lama tidak bisa diedit langsung; buat secret baru dengan nama berbeda, lalu perbarui service supaya memakai secret yang baru dan hapus secret lama setelah tidak ada service yang memakainya.

docker service update \
  --secret-rm db_password \
  --secret-add source=db_password_v2,target=db_password \
  payment-api

docker secret rm db_password

Pastikan tidak ada service lain yang masih memakai secret lama sebelum menghapusnya; perintah docker secret rm akan gagal dan menampilkan error kalau secret tersebut masih terpakai, sebuah pengaman bawaan Swarm supaya rotasi secret tidak sampai membuat service yang sedang berjalan kehilangan akses secara tiba-tiba.

docker secret ls

24.4.4 Integrasi dengan External Secret Manager

Untuk organisasi yang sudah punya kebijakan manajemen credential terpusat, secret sebaiknya tidak disimpan sebagai file statis di server sama sekali, melainkan diambil secara dinamis dari external secret manager seperti HashiCorp Vault, AWS Secrets Manager, atau Google Secret Manager saat container baru mulai berjalan. Pola yang umum dipakai di lapangan adalah init container atau sidecar yang mengambil secret dari manager eksternal lewat API terautentikasi, lalu menuliskannya ke volume tmpfs yang dibagikan ke container aplikasi utama, sehingga aplikasi tetap membaca secret dari file lokal tanpa perlu tahu detail integrasi ke secret manager tersebut.

Pendekatan ini memindahkan tanggung jawab autentikasi, rotasi, dan audit trail akses secret sepenuhnya ke sistem yang memang dirancang khusus untuk itu, alih-alih mengandalkan file secret statis yang gampang lupa dirotasi atau tanpa sadar ikut tersebar lewat backup maupun snapshot volume. Keputusan memakai secret manager eksternal biasanya diambil bersama tim keamanan organisasi, karena implementasinya menyentuh kebijakan identity dan access management yang lebih luas daripada sekadar konfigurasi Docker semata.