Menggunakan Checkpoint Di Kubernetes Untuk Forensik
Pada tulisan Mematikan Pod Secara Otomatis Saat Tracee Mendeteksi Bahaya, saya menerapkan incident response (IR) dengan mematikan Pod secara otomatis. Pada tulisan ini, saya akan berfokus pada digital forensik (DF)-nya. Bila Pod telah dimatikan dan lenyap, bagaimana caranya melakukan forensik untuk memahami apa yang terjadi? Salah satu fitur Kubernetes yang bisa saya pakai untuk keperluan ini adalah Checkpoint API. Checkpoint dirancang khusus untuk bekerja pada container yang sedang berjalan tanpa mempengaruhi container tersebut. Oleh sebab itu, saya perlu membuat checkpoint sebelum mematikan Pod.
Forensic Container Checkpointing
Fitur Forensic Container Checkpointing ditambahkan di Kubernetes pertama kali pada versi 1.25. Pada saat itu, fitur ini memiliki status alpha sehingga perlu diaktifkan secara manual bila ingin dipakai. Forensic Container Checkpointing memasuki tahap beta di Kubernetes versi 1.30 sehingga bila saya menggunakan Kubernetes versi 1.30 ke atas, fitur ini sudah aktif secara bawaan.
Forensic Container Checkpointing di Kubernetes menggunakan CRIU untuk menyimpan salinan isi container yang sedang berjalan mulai dari isi memori untuk setiap proses yang sedang berjalan, setiap file baru yang dibuat diluar image yang dipakai, dan metadata container tersebut. Hasil dari checkpoint secara default akan disimpan di lokasi /var/lib/kubelet/checkpoints
. Isi folder ini dapat dipindahkan ke komputer lain untuk analisa forensik digital.
Pada tulisan Mematikan Pod Secara Otomatis Saat Tracee Mendeteksi Bahaya, saya menggunakan Kubernetes API (kube-apiserver
) untuk mendapatkan daftar Pod, mematikan Pod dan sebagainya. Namun, saya tidak bisa membuat checkpoint dengan Kubernetes API. Sebagai gantinya, saya harus memangggil Kubelet API (kubelet
).
Setiap node di cluster Kubernetes menyediakan Kubelet API di port 10250
dengan tujuan untuk dipanggil oleh control plane. Karena bukan untuk dipanggil oleh pengguna secara langsung, endpoint di Kubelet API tidak didokumentasikan secara resmi dan bisa berubah seiring waktu tanpa pemberitahuan. Namun, untuk saat ini, saya wajib menggunakan Kubelet API karena fitur checkpoint hanya tersedia di endpoint POST /checkpoint/{namespace}/{pod}/{container}
, saya wajib menggunakan Kubelet API.
Checkpoint API bekerja pada level container sehingga saya perlu mengetahui nama container yang akan dibuat checkpoint-nya. Ssebuah Pod dapat memiliki lebih dari satu container, misalnya pada saat menggunakan Istio, Pod memiliki tambahan sidecar container yang dibuat otomatis. Deteksi yang dilakukan oleh Tracee juga berlaku pada level container, misalnya, jika aktifitas mencurigakan ada di sidecar container (bukan di container aplikasi) maka log Tracee akan berisi container id untuk sidecar container tersebut.
Minikube
Walaupun Kubelet API sudah mendukung Checkpoint API, container runtime yang dipakai juga wajib mendukung fitur checkpoint. Bila container runtime tidak mendukung, saat memanggil Checkpoint API, saya akan mendapatkan respon 500 - Internal Server Error
. Setelah menelusuri log Kubelet (misalnya dengan journalctl -u kubelet
), saya akan menemukan pesan kesalahan seperti CheckpointContainer not implemented
.
Pada saat tulisan ini dibuat, seluruh container runtime bawaan Minikube v1.34.0 yang saya pakai tidak ada yang mendukung fitur checkpoint. Hal ini berarti pemanggilan Checkpoint API dari Kubernetes akan selalu gagal. Satu-satunya container runtime yang paling mendekati adalah CRI-O. Minikube v1.34.0 datang dengan CRI-O versi 1.24, sementara itu dukungan checkpoint di CRI-O ditambahkan di versi 1.25. Karena hanya beda satu versi minor, saya akan mencoba meng-upgrade versi CRI-O yang dipakai oleh Minikube.
Karena saya menggunakan Docker driver untuk mensimulasikan node Kubernetes di Minikube, saya akan men-build image kicbase
secara manual. Untuk itu, saya perlu men-download source code Minikube yang ada di https://github.com/kubernetes/minikube. Langkah pertama yang saya lakukan adalah mengaktifkan CRIU di CRI-O. Saya dapat melakukannya dengan menambahkan baris berikut ini pada file deploy/kicbase/02-crio.conf
:
Setelah itu, saya men-build image kicbase
dengan menyertakan argumen CRI_VERSION
berupa nilai 1.25
seperti pada berikut ini:
Sampai disini, saya akan mendapatkan image kicbase
lokal yang sudah menggunakan CRI-O 1.25 dengan dukungan checkpoint yang sudah diaktifkan. Agar Minikube dapat menggunakan image ini, saya perlu meletakkan image ini ke sebuah registry publik. Sebagai contoh, saya bisa menggunakan registry lokal dengan perintah seperti berikut ini:
Saya kemudian membuat cluster Kubernetes baru di Minikube dengan perintah seperti berikut ini:
Setelah cluster Kubernetes dibuat, saya dapat memastikan versi container runtime yang dipakai sudah benar dengan memberikan perintah seperti berikut ini:
Saya perlu memastikan bahwa kolom STATUS
bernilai Ready
dan nilai CONTAINER-RUNTIME
adalah cri-o://1.25.4
yang sudah mendukung checkpoint.
RBAC
Untuk authorization Kubelet, saya dapat menggunakan nama resource nodes
, misalnya nodes/metrics
untuk akses ke metrics, nodes/logs
untuk akses ke logs, dan nodes/checkpoint
untuk akses ke checkpoint. Karena ingin menggunakan fitur checkpoint, saya akan menambahkan nodes/checkpoint
ke ClusterRole yang sebelumnya saya pakai:
Kode Program
Saya akan melakukan perubahan kode program alert-responder
yang saya buat di tulisan Mematikan Pod Secara Otomatis Saat Tracee Mendeteksi Bahaya.
Untuk memanggil Checkpoint API, saya membutuhkan informasi berupa namespace, nama Pod dan nama container di dalam Pod. Log dari Tracee sudah menyediakan informasi container di property containerId
dan container.id
. Saya bisa menambahkan function berikut ini untuk mendapatkan nilai container id yang diasosiasikan dengan alert yang sedang aktif:
Setelah mendapatkan container id, saya perlu menerjemahkannya menjadi container name yang dibutuhkan oleh Checkpoint API. Saya bisa mendapatkan seluruh daftar Pod yang berjalan dengan memanggil Kubernetes API dan memeriksa setiap Pod tersebut apakah memiliki container id bersangkutan. Dengan cara ini, saya juga bisa mengisi nama pod, namespace, dan nama node yang menjalankan container id. Cara ini lebih handal bila dibandingkan dengan berharap pada nama Pod dan namespace dari log Tracee yang terkadang isinya kosong.
Untuk itu, saya akan membuat kode program seperti berikut ini:
Tidak seperti Kubernetes API yang bersifat tunggal untuk keseluruhan cluster, masing-masing node memiliki Kubelet API tersendiri. Oleh sebab itu, saya perlu memanggil Kubelet API di IP node yang menjalankan container bersangkutan. Untuk mendapatkan IP node dan port Kubelet API berdasarkan nama node, saya bisa menggunakan Kubernetes API seperti pada contoh kode program berikut ini:
Sampai disini, saya sudah mendapatkan semua informasi yang dibutuhkan untuk memanggil Checkpoint API. Saatnya menulis kode program untuk melakukan checkpoint. Karena Checkpoint API bukan bagian dari Kubernetes API, saya tidak bisa menggunakan Kubernetes Go client seperti pada kode program sebelumnya. Sebagai gantinya, saya akan memanggil Checkpoint API dengan http.NewRequest
bawaan Go seperti pada kode program berikut ini:
Pada kode program di atas, saya membaca service account yang di-mount secara otomatis oleh Kubernetes di lokasi /var/run/secrets/kubernetes.io/serviceaccount/token
. Service account ini akan memiliki authorization sesuai dengan konfigurasi RBAC yang telah saya buat sebelumnya. Untuk menggunakannya, saya cukup melewatkan nilai service account tersebut sebagai nilai pada header Authorization
. Tanpa header ini, pemanggilan ke Checkpoint API akan gagal dengan error 401 - Unauthorized
.
Sebagai langkah terakhir, saya kemudian bisa mengubah kode program yang menangani queue supaya memanggil CreateCheckPoint()
terlebih dahulu sebelum memanggil Kill()
:
Sekarang, sebelum alert-responder
mematikan Pod, ia akan membuat checkpoint terlebih dahulu. Saya bisa menemukan hasil checkpoint-nya dalam bentuk file .tar
di folder /var/lib/kubelet/checkpoints
di node bersangkutan:
Analisa Forensik
Setelah memindahkan file checkpoint-nginx-749f68f68f-qwfdk_default-nginx-2025-01-06T20:41:51Z.tar
dari node sebagai file forensic.tar
di mesin lain, saya dapat mulai melakukan analisa. Walaupun saya bisa membaca isi file satu per satu, cara yang lebih gampang adalah dengan menggunakan tool checkpointctl
. Sebagai contoh, untuk melihat daftar proses yang berjalan di container, saya dapat memberikan perintah seperti berikut ini:
Untuk melihat socket yang dipakai oleh setiap proses di dalam container, saya dapat memberikan perintah:
Untuk melihat isi memori untuk masing-masing process id (PID), saya dapat memberikan perintah seperti berikut ini:
Untuk melihat apa saja file baru yang ditambahkan diluar image bawaan container, saya dapat melihat isi folder rootfs-diff
pada hasil forensic.tar
yang telah di-extract:
Directoryetc
- mtab
Directoryrun
Directorysecrets
Directorykubernets.io
Directoryserviceaccount
- …
- nginx.pid
Directoryvar
Directorylib
Directorynginx
- …
Terlihat bahwa ada beberapa file baru di container seperti /run/nginx.pid
dan folder di /var/lib/nginx
. Karena seluruh file baru tersebut disimpan pada hasil checkpoint, bila ada yang mencurigakan, saya dapat langsung memeriksa isinya.