Mematikan Pod Secara Otomatis Saat Tracee Mendeteksi Bahaya
Pada suatu hari, saya membaca tentang proyek falco-talon yang disebut sebagai Response Engine untuk threats di Kubernetes. Saya sudah pernah memakai falco untuk mendeteksi keanehan pada setiap Pod yang berjalan di Kubernetes. Dengan falco-talon
, setiap alert dari falco
dapat diubah menjadi respon otomatis seperti men-kill (mematikan) Pod bersangkutan, meng-isolasi cluster dengan fitur cordon dari Kubernetes, dan sebagainya. Lalu bagaimana bila saya menggunakan Tracee? Terinspirasi oleh falco-talon
, saya akan mencoba menambahkan fasilitas respon otomatis yang akan men-kill Pod berdasarkan alert dari Tracee. Karena biasanya Pod dikelola oleh Deployment, Kubernetes akan membuat ulang Pod yang di-kill secara otomatis, sehingga operasi ini relatif aman.
Kode Program
Saya akan mulai dengan membuat sebuah aplikasi Go yang akan menerima webhook dari Tracee. Aplikasi ini kemudian akan men-kill Pod yang terkait dengan alert tersebut dengan menggunakan API Kubernetes. Jenis aplikasi ini adalah in-cluster karena aplikasi yang memanggil API Kubernetes berada di cluster yang sama. Ini lebih aman karena service account disuntikkan langsung ke Pod tanpa harus disimpan terpisah (yang rentan terhadap kebocoran).
Untuk mempermudah pemanggilan API Kubernetes, saya akan menggunakan client resmi Kubernetes untuk Go dengan menjalankan perintah berikut ini:
Saya kemudian akan membuat file dengan stuktur folder seperti berikut ini:
Directorycmd
Directoryalert-responder
- main.go
Directoryinternal
Directorylog
- tracee_log.go
Directorypod
- pod_killer.go
- Dockerfile
pod_killer.go
Kode program pod_killer.go
terlihat seperti berikut ini:
Karena ada kemungkinan aplikasi ini dipanggil berulang kali dalam waktu singkat, agar tetap responsif (bisa mengembalikan hasil paling lama 5 detik), saya menggunakan channel sebagai buffer dengan definisi seperti make(chan Pod, 100)
. Penggunaan for
pada StartQueue()
tidak akan pernah selesai, sehingga StartQueue()
perlu dipanggil di sebuah thread terpisah dengan goroutine. Dengan demikian, proses penghapusan Pod akan dilakukan di thread tersendiri yang masih akan diproses bahkan setelah HTTP request dari webhook sudah selesai.
Untuk menambahkan Pod yang perlu dihapus, pada kode program di atas, saya membuat function AddPodToKill()
. Pada function tersebut, saya menggunakan select
saat mengirim pod
ke channel sehingga sifatnya adalah non-blocking. Baris queue <- pod
akan langsung lanjut ke perintah berikutnya tanpa menunggu dibaca. Selain itu, karena isi default
di select
kosong, bila seandainya buffer sudah penuh (terdapat 100 Pod yang masih belum diproses), Pod baru akan diabaikan begitu saja.
Sebelum dapat memanggil Kubernetes API, saya perlu melakukan inisialisi client terlebih dahulu. Pada kode program di atas, karena Kubernetes API dipanggil dari dalam cluster secara internal, saya menggunakan rest.InClusterConfig()
. Kode program yang menghapus Pod cukup sederhana dah hanya satu baris dalam bentuk seperti clientset.CoreV1().Pods(nama_namespace).Delete(context.TODO(), nama_pod, metav1.DeleteOptions{})
.
tracee_log.go
Kode program tracee_log.go
terlihat seperti berikut ini:
Kode program di atas pada dasar melakukan parsing JSON yang dikirim oleh Tracee menjadi map
di variabel req
. Saya perlu mengambil nilai kubernetes.podName
dan kubernetes.podNamespace
. Hanya alert yang datang dari Pod (di dalam container) yang akan memiliki nilai tersebut. Alert dari host tidak akan memiliki nilai kubernetes
dan container
.
Selain itu, sangat tidak realistis untuk melakukan respon otomatis untuk setiap alert dari Tracee karena ada sangat banyak false positive. Sebagai latihan, saya hanya akan menghapus Pod secara otomatis bila terdeteksi signature TRC-101
yang mendeteksi operasi reverse shell.
main.go
Kode program main.go
terlihat seperti berikut ini:
Kode program di atas pada dasarnya menjalankan web server di port 8080
yang akan dipanggil sebagai webhook bagi Tracee.
Setelah selesai menulis kode program, saya menjalankan perintah berikut ini untuk memastikan bahwa seluruh dependency sudah ditambahkan di file go.mod
:
Deployment
Image
Untuk membuat image dari kode program di atas, saya akan menambahkan Dockerfile
dengan isi seperti berikut ini:
Pada deklarasi di atas, saya menggunakan multi-stage build dimana aplikasi alert-responder
di-build dengan base image Go 1.23.1. Setelah file binary berhasil dibuat, ia akan di-deploy di base image distroless dengan user nonroot
. Dengan demikian, saya tidak perlu menyertakan instalasi Go pada image yang dihasilkan.
Saya kemudian membuat image dengan perintah seperti berikut ini:
RBAC
Untuk menjalankan image ini di Kubernetes, saya perlu membuat manifest-nya. Karena Kubernetes modern kebanyakan sudah mengaktifkan RBAC dan image di atas membutuhkan permission untuk menghapus pod secara global, saya akan mendefinisikan objek untuk RBAC terlebih dahulu pada file manifest:
Pada definisi RBAC di atas, saya mendefinisikan sebuah ServiceAccount yang akan dipakai oleh Pod nantinya. Saya juga membuat role baru dengan jenis ClusterRole karena saya ingin alert-responder
bisa mematikan (kill) Pod yang berada dimana saja. Bila ingin membatasi Pod yang bisa dimatikan hanya di namespace tertentu, saya bisa mengganti ClusterRole dengan Role.
Deployment & Service
Setelah itu, agar bisa menjalankan image sebagai Deployment di Kubernetes, saya akan menambahkan definisi berikut ini:
Definisi di atas akan membuat sebuah Pod tunggal untuk menjalankan alert-responder
di namespace tracee
. Pod ini memiliki port 8080
yang bisa digunakan untuk mengakses aplikasi. Agar port ini bisa dipanggil oleh Tracee nantinya, saya perlu membuat headless service seperti pada definisi berikut ini:
Perbedaan headless service dan Service biasa adalah nilai clusterIP
-nya yang berupa None
. Dengan demikian, headless service tidak bisa diakses dari luar melalui alamat IP (seperti diakses secara publik lewat load balancer). Walaupun demikian, headless service tetap bisa dipanggil oleh workload lainnya dengan menggunakan DNS record internal yang dikelola Kubernetes dengan nama seperti [nama headless service].[nama namespace].svc.[domain cluster]
. Dengan demikian, Pod alert-responder
yang saya buat bisa dipanggil dengan menggunakan nama berupa alert-responder.tracee.svc.cluster.local
.
Terakhir, saya akan menambahkan sebuah Deployment nginx
yang akan dijadikan sebagai target pengujian dengan definisi seperti berikut ini:
Saya bisa men-deploy manifest Kubernetes yang telah saya tulis dengan menggunakan perintah seperti berikut ini:
Tracee
Selanjutnya, saya perlu melakukan perubahan pada file konfigurasi Tracee dengan menambahkan webhook
pada nilai output
seperti berikut ini:
Bila ini adalah cluster kosong, saya bisa mengatur nilai webhook ini pada saat melakukan instalasi Tracee lewat Helm dengan perintah seperti berikut ini:
Sampai disini, proses deployment sudah selesai dan saya siap melakukan pengujian untuk memastikan alert-responder
bekerja sesuai dengan harapan!
Pengujian
Untuk melakukan pengujian, saya akan mencoba membuat reverse shell pada Pod nginx
. Langkah pertama yang saya lakukan adalah membuat listener di sebuah server publik dengan perintah seperti berikut ini:
Lalu, saya akan masuk ke dalam Pod nginx
dengan menggunakan perintah berikut ini:
Pada perintah di atas, saya perlu mengganti nilai 10.10.10.10
dengan IP server publik yang saya pakai. Namun, begitu saya menekan tombol Enter, saya langsung menemukan perintah command terminated
dan shell akan ditutup. Bila saya menggunakan perintah kubectl get pods
, saya akan menemukan bahwa Pod yang saya pakai sudah tidak ada, digantikan oleh Pod yang masih baru (terlihat dari nilai di kolom AGE
). Dengan demikian, aplikasi tidak akan down (nginx
masih tetap bisa diakses).