Mengamankan container di level kernel: integrasi AppArmor dengan Docker dan containerd, profile docker-default, profile kustom lewat security-opt, serta pemasangan profile ke pod Kubernetes lewat annotation dan native field.

Di episode 9, kalian mengunci layanan yang berjalan langsung di host. Tapi ada kategori workload yang tidak kalah penting: container. Daya tarik container adalah berbagi kernel — dan ironisnya, di situlah letak bahayanya. Namespace dan cgroups mengisolasi container satu sama lain, tetapi proses container tetap berjalan di kernel yang sama dengan host. Jika penyerang keluar dari aplikasi di dalam container, batas berikutnya adalah kernel itu sendiri.
AppArmor menutup celah ini dari level kernel: setiap proses container bisa dibatasi oleh profile yang sama persis dengan proses host. Episode 10 ini membahas bagaimana Docker, containerd, dan Kubernetes bekerja sama dengan AppArmor.
Docker memuat profile AppArmor ke kernel menggunakan apparmor_parser, lalu memasangnya ke proses container saat start. Cek dulu apakah runtime kalian mendeteksi AppArmor:
docker info --format '{{.SecurityOptions}}'Jika output menyertakan name=apparmor, AppArmor aktif. Docker juga membawa satu profile bawaan bernama docker-default yang dipasang otomatis ke setiap container yang tidak meminta profile lain. Profile ini sudah menyediakan deny by default untuk banyak operasi berbahaya: mount, capability tertentu, dan beberapa path sistem.
Note
docker-default adalah jaring pengaman minimum, bukan pengganti profile khusus. Ia menolak hal-hal yang jelas berbahaya, tapi tidak memahami aplikasi kalian. Container web yang bisa menulis ke seluruh host tetap bisa melakukannya selama docker-default mengizinkan. Untuk perlindungan nyata, kalian butuh profile kustom.
Container punya keunikan dibanding proses host: root filesystem diganti lewat pivot, sehingga profile berbasis path executable biasa sering gagal menangkap proses container. Solusinya adalah profile berbasis nama dengan flag khusus:
#include <tunables/global>
profile myapp-profile flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network inet tcp,
capability setgid,
capability setuid,
/app/** r,
/app/bin/myapp mr,
/tmp/** rw,
}Dua hal yang perlu diperhatikan:
profile myapp-profile — profile diberi nama, bukan path executable. Kernel akan memasangnya berdasarkan label yang diberikan runtime.flags=(attach_disconnected,mediate_deleted) — dibutuhkan karena di dalam container file-file tertentu terlihat "terputus" dari path aslinya; flag ini membuat pengecekan tetap berjalan normal.Sebelum dipakai, profile harus dimuat ke kernel:
sudo apparmor_parser -a /etc/apparmor.d/myapp-profileKonfirmasi dengan aa-status bahwa myapp-profile sudah terdaftar sebelum menjalankan container — profil yang tidak termuat akan diam-diam diganti docker-default oleh Docker.
Lalu jalankan container dengan profile itu:
docker run --rm --security-opt apparmor=myapp-profile myapp:v1Flag --security-opt apparmor=... memberitahu runtime profile mana yang dipasang. Tanpa flag ini, Docker memakai docker-default.
containerd — runtime yang dipakai Kubernetes — mendukung AppArmor lewat CRI (Container Runtime Interface). Profile yang diminta pod akan dimuat otomatis ke kernel sebelum container start. Untuk memakai containerd langsung tanpa Kubernetes, ctr menyediakan flag --apparmor-profile dan --apparmor-default-profile, sementara profile default untuk seluruh node bisa disetel di /etc/containerd/config.toml. Cara paling umum tetap lewat CRI: annotation atau securityContext yang diminta pod diterjemahkan runtime menjadi pemasangan profile.
Kubernetes memilih profile AppArmor per container. Selama bertahun-tahun, caranya adalah annotation dengan awalan container.apparmor.security.beta.kubernetes.io/ diikuti nama container:
apiVersion: v1
kind: Pod
metadata:
name: myapp
annotations:
container.apparmor.security.beta.kubernetes.io/myapp: localhost/myapp-profile
spec:
containers:
- name: myapp
image: myapp:v1Nilai yang valid:
runtime/default — pakai profile bawaan runtime (di containerd, ini meneruskan ke profile AppArmor default yang dikonfigurasi).localhost/<nama> — pakai profile yang sudah dimuat di node, misalnya myapp-profile yang kita load lewat apparmor_parser.unconfined — lewati AppArmor (hindari kecuali ada alasan kuat).Annotation ini masih didukung, tetapi arah Kubernetes adalah native field di securityContext. Versi Kubernetes modern mendukung objek appArmorProfile:
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: myapp-profiletype: RuntimeDefault memakai profile bawaan runtime, sedangkan type: Localhost memakai profile yang tersedia di node dan ditunjuk lewat localhostProfile. Native field menandai masa depan — jika versi Kubernetes kalian mendukungnya, pakai ini daripada annotation.
Warning
Profile kustom harus ada di setiap node tempat pod bisa dijadwalkan. Kubelet tidak meng-upload profile — ia hanya memilih dari yang sudah dimuat di kernel node tersebut. Bawa profile sebagai bagian dari provisioning node (misalnya lewat Ansible atau konfigurasi node image), bukan dikelola per-pod.
Pada episode 10 ini kalian sudah mengamankan container dari level kernel: integrasi Docker lewat apparmor_parser dan --security-opt apparmor=..., peran docker-default sebagai jaring pengaman, dukungan containerd/CRI, serta dua cara memasang profile di Kubernetes — annotation container.apparmor.security.beta.kubernetes.io dan native field securityContext.appArmorProfile.
Kunci yang harus dibawa pulang:
flags=(attach_disconnected,mediate_deleted).Sejauh ini semua contoh berbau server dan container. Tapi AppArmor juga menjaga mesin yang kalian pakai setiap hari — desktop dengan browser, office, aplikasi snap, dan layanan sistem yang menyalakan distro. Di episode 11, kita menutup ranah ini di Desktop & System Services: Firefox, LibreOffice, integrasi snap dan flatpak, konflik saat update, serta profile layanan sistem seperti networking, DBus, dan systemd.