Mengamankan virtualisasi QEMU/KVM lewat domain sVirt (svirt_t), pelabelan disk image dengan virt_image_t dan virt_content_t, dan penanganan konteks keluarga virt. Kemudian menyusun policy kustom: menulis modul Type Enforcement, kompilasi dengan checkmodule dan semodule_package, hingga authoring CIL.

Di episode 11 kita menutup ranah containers — Podman, Docker, dan Kubernetes — tempat SELinux membatasi kontainer yang sesungguhnya berbagi kernel dengan host. Kali ini kita naik satu tingkat berikutnya: virtualisasi dengan QEMU/KVM. Sekilas sebuah VM terlihat "lebih aman" karena membawa kernelnya sendiri, tapi ancaman terbesar virtualisasi bukanlah kernel guest — melainkan hypervisor. Jika QEMU yang mengeksekusi sebuah VM berhasil ditembus (VM escape), attacker langsung berdiri di host, dan seluruh isolasi antar VM menjadi tidak berarti.
SELinux menutup celah ini lewat domain sVirt serta isolasi berbasis MCS — dua hal yang sudah kita kenali dari episode 6 dan 9. Di paruh kedua episode, kita berubah peran: dari operator yang mengamankan mesin, menjadi penulis policy. Kita akan menyusun modul policy kustom dari nol: menulis source Type Enforcement, mengompilasinya dengan checkmodule dan semodule_package, memuatnya lewat semodule, lalu membahas cara authoring yang lebih modern dengan CIL.
Bayangkan hypervisor sebagai concierge gedung yang memegang semua kunci kamar. Seorang penjahat yang berhasil mengambil alih concierge otomatis menguasai semua kamar. Dalam konteks virtualisasi, "kamar" adalah VM-nya, dan "concierge" adalah proses qemu-kvm yang berjalan sebagai proses biasa di host. Di sinilah letak ironinya: attacker yang mengeksploitasi guest tidak perlu menyerang host lewat jaringan — ia cukup mencari jalan keluar dari QEMU.
SELinux memutus skenario ini di akarnya dengan mengkonfinasi QEMU itu sendiri. QEMU berjalan dalam domain terbatas, sehingga meskipun berhasil di-escape, tindakan yang bisa dilakukannya terhadap host dibatasi oleh policy — persis seperti kontainer di episode 11, tapi dengan lapisan tambahan berupa isolasi antar VM.
Saat libvirt meluncurkan sebuah VM, proses QEMU diberi salah satu dari dua domain utama:
svirt_t — untuk VM yang berjalan dengan akselerasi hardware KVM. Ini domain yang paling umum di produksi.svirt_tcg_t — untuk VM yang berjalan dengan emulasi perangkat lunak (TCG, tanpa /dev/kvm). Perhatikan, domain ini lebih terbatas daripada svirt_t.Coba verifikasi sendiri pada host yang punya VM:
ps -eZ | grep qemusystem_u:system_r:svirt_t:s0:c109,c577 1234 ? 00:10:00 qemu-kvm -name webserver ...Perhatikan bagian terakhir konteksnya: s0:c109,c577. Itu adalah label MCS — kategori yang unik per VM. Inilah kunci keamanan sVirt: setiap VM hanya boleh menyentuh resource yang membawa kategori miliknya. VM A dengan c109,c577 tidak akan pernah diizinkan membaca disk image VM B yang berlabel c23,c870, sekalipun QEMU A berhasil di-escape dan berusaha mem-bypass hypervisor. Bayangkan ini sebagai nomor kunci ber-flag yang berbeda untuk setiap kamar di gedung yang sama.
Tip
Domain svirt_tcg_t sengaja dibuat lebih lemah daripada svirt_t. Sebagai administrator, ini sinyal yang bagus: virtualisasi tanpa KVM dianggap lebih berisiko, sehingga policy mengecilkan permukaan serangnya secara otomatis.
Seperti filesystem biasa (episode 5), file-file yang dipakai VM harus berlabel sesuai dengan domain yang akan membacanya. Tiga label paling penting:
| Jenis file | Label | Lokasi umum |
|---|---|---|
| Disk image VM | virt_image_t | /var/lib/libvirt/images/ |
| File ISO / media read-only | virt_content_t | /var/lib/libvirt/isos/ |
| File lain milik libvirt | virt_var_lib_t | /var/lib/libvirt/ |
Periksa label disk image kalian:
ls -Z /var/lib/libvirt/images
matchpathcon /var/lib/libvirt/images/webserver.qcow2Jika ada file yang berlabel salah — misalnya karena dipindahkan dari direktori lain, atau dibuat dengan cp tanpa label — QEMU akan mendapat denial saat membukanya. Perbaiki dengan mengembalikan label sesuai konteks default, atau menandai ulang secara manual:
restorecon -v /var/lib/libvirt/images/webserver.qcow2
chcon -t virt_image_t /var/lib/libvirt/images/webserver.qcow2restorecon memakai konteks default dari semanage fcontext, sedangkan chcon memaksa label sesaat. Untuk lokasi penyimpanan kustom di luar /var/lib/libvirt, daftarkan konteksnya dengan semanage fcontext:
semanage fcontext -a -t virt_image_t '/data/vms(/.*)?'
restorecon -R -v /data/vmsPerhatikan pola (/.*)? — ini regex yang memastikan seluruh isi direktori ikut berlabel virt_image_t, bukan hanya direktori induknya.
Keluarga konteks virt- dan svirt- sering membingungkan. Yang perlu kalian ingat: svirt_* untuk domain (proses), virt_* untuk objek (file, soket, direktori). Beberapa konteks objek lain yang sering muncul:
virt_log_t — log di /var/log/libvirt/virt_tmp_t dan virt_tmpfs_t — file sementara VMsvirt_socket_t — soket Unix yang dipakai komunikasi antar proses QEMUJika sebuah denial muncul dengan tcontext berlabel svirt_socket_t atau virt_var_lib_t, hampir selalu penyebabnya file yang labelnya tidak konsisten dengan lokasinya — bukan bug policy. Langkah pertama yang benar adalah restorecon, bukan menulis aturan baru. Aturan ini sama dengan episode 5: sembilan dari sepuluh masalah labeling adalah label yang tidak sesuai dengan konteks default.
Di episode 8 kita memakai audit2allow untuk menerjemahkan denial menjadi modul lokal. Itu pendekatan reaktif — policy lahir dari log. Sekarang kita berpikir proaktif: bagaimana jika kalian men-deploy aplikasi buatan sendiri yang belum punya domain sama sekali? Tidak ada jenis denial yang menuntun kalian, karena aplikasinya belum pernah dijalankan. Di sinilah kita menulis policy sendiri: mendeklarasikan domain baru, memberi tahu SELinux file eksekusi mana yang memicunya, dan menyatakan interaksi yang diizinkan.
Modul TE klasik terdiri dari tiga bagian: header module, blok require, dan deklarasi aturan. Contoh aplikasi daemon myapp yang harus bisa name_connect ke port HTTP:
policy_module(myapp, 1.0.0);
require {
type http_port_t;
class tcp_socket { accept bind connect listen name_bind name_connect };
class process { transition sigchld };
}
type myapp_t;
type myapp_exec_t;
init_daemon_domain(myapp_t, myapp_exec_t);
allow myapp_t http_port_t:tcp_socket name_connect;Mari kita bedah:
policy_module(myapp, 1.0.0) — nama dan versi modul. Nama ini yang muncul di semodule -l.require — deklarasi tipe dan class yang kita pakai tapi dimiliki modul lain. Kita tidak boleh mendefinisikan ulang http_port_t; kita cukup menyatakan "saya butuh ini".type myapp_t dan type myapp_exec_t — domain proses dan tipe file eksekusi. Dua tipe berbeda, peran berbeda.init_daemon_domain(...) — interface bawaan refpolicy yang menyiapkan transisi daemon: saat myapp_exec_t dieksekusi dari konteks yang mengizinkan, prosesnya otomatis masuk myapp_t.allow myapp_t http_port_t:tcp_socket name_connect; — jantung aturan: domain myapp_t boleh membuka koneksi TCP keluar ke port berlabel http_port_t.Kompilasi modul TE menjadi policy package .pp, lalu muat:
checkmodule -M -m -o myapp.mod myapp.te
semodule_package -o myapp.pp -m myapp.mod
semodule -i myapp.pp
semodule -l | grep myappcheckmodule -M -m mengompilasi source TE menjadi modul perantara .mod (-M menandakan format module, bukan base policy).semodule_package membungkus .mod menjadi .pp (mudah didistribusikan, bisa digabung dengan file context).semodule -i memasangnya ke policy aktif — berlaku langsung tanpa reboot.Warning
Jangan mengedarkan modul .te yang belum kalian uji di mode permissive. Satu aturan allow yang salah bisa membuka lebih banyak akses dari yang kalian bayangkan. Kebiasaan profesional: uji domain baru di mesin staging dengan SELinux permissive, tangkap denials-nya dengan ausearch, sempurnakan modul, baru pasang di produksi.
CIL (Common Intermediate Language) adalah cara yang lebih baru dan lebih ekspresif untuk menulis policy — dibahas lengkap di episode 17. Untuk episode ini cukup kenalan: modul CIL ditulis dengan kurung S-expression, dan yang menarik, banyak struktur yang di TE tersembunyi di balik interface (seperti init_daemon_domain) bisa ditulis eksplisit di CIL:
(type myapp_t)
(type myapp_exec_t)
(roletype system_r myapp_t)
(typeattributeset cil_gen_require (http_port_t))
(allow myapp_t http_port_t (tcp_socket (name_connect)))
(allow myapp_t self (process (sigchld)))Baris typeattributeset cil_gen_require (http_port_t) adalah mekanisme CIL untuk menyatakan dependensi ke tipe eksternal — setara blok require di TE. Kompilasi dengan secilc lalu pasang seperti biasa:
secilc -o myapp.cil.bin myapp.cil
semodule -i myapp.cilPerhatikan: semodule -i menerima file .cil mentah maupun hasil kompilasi .cil.bin. CIL tidak hanya "bentuk lain" dari TE — ia menyederhanakan banyak idiom yang menyakitkan di TE, dan menjadi bahasa pengantar dari semua policy modern. Inilah alasan kita kembali padanya secara mendalam di episode 17.
Pada episode 12 ini kalian telah mengamankan virtualisasi dari dua sisi: sebagai operator, memahami bahwa QEMU dikonfinasi dalam domain svirt_t/svirt_tcg_t dengan isolasi MCS per VM (s0:c109,c577), bahwa disk image harus berlabel virt_image_t, ISO berlabel virt_content_t, dan bahwa keluarga konteks virt_*/svirt_* punya pola yang konsisten (restorecon dulu, baru menulis aturan). Sebagai penulis policy, kalian juga sudah menyusun modul TE pertama: blok require, deklarasi type, interface init_daemon_domain, kompilasi dengan checkmodule/semodule_package, hingga alternatif modern dengan CIL dan secilc.
Inti yang harus kalian bawa:
virt_image_t, virt_content_t) berbeda dari label domain (svirt_t) — jangan tertukar.allow yang eksplisit dan sesedikit mungkin.Sekarang QEMU dan aplikasi kustom kalian berjalan dengan konteks yang terkendali. Tapi ada satu jenis objek yang belum pernah kita label: jaringan. Sebuah daemon yang boleh name_connect ke http_port_t — bagaimana SELinux tahu port mana yang masuk kategori itu? Itu bukan ditentukan oleh kernel, melainkan oleh data pelabelan yang bisa kalian ubah. Di episode 13 berikutnya kita masuk ke Network Port & Socket Labeling: mengelola semanage port, memetakan service ke port type, dan pelabelan paket serta koneksi di netfilter. Sampai jumpa di episode 13!