Berinteraksi Dengan Socket containerd Dari Dalam Pod
Anggap saja saya berhasil mendapatkan shell di Pod yang men-mountcontainerd.sock dari host ke dalam Pod tersebut. File containerd.sock adalah Unix socket yang disediakan untuk berinteraksi dengan containerd (sama seperti file docker.sock yang dipakai oleh perintah docker). Apa yang bisa saya lakukan dengan file socket tersebut?
Container Runtime Interface (CRI) adalah komponen Kubernetes yang berkomunikasi dengan container runtime. Dengan CRI, Kubernetes tidak perlu terikat pada satu jenis container runtime. Pada awalnya, container runtime yang dipakai oleh Kubernetes adalah Docker. Namun, saat ini status Docker sebagai CRI sudah deprecated. Salah satu calon penerusnya adalah containerd. Ia merupakan alternatif yang paling banyak dipakai (misalnya containerd adalah CRI default di GKE).
Bila dilihat dari luar, tidak ada perubahan berarti karena developer tetap menggunakan Docker untuk membuat image baru. Namun, khusus untuk aplikasi yang perlu berinteraksi langsung dengan container runtime seperti monitoring agent, lokasi file socket untuk containerd adalah /run/containerd/containerd.sock (yang juga tersedia di /var/run/containerd/containerd.sock). Lokasi ini berbeda dengan socket Docker yang ada di /var/run/docker.sock.
Sebagai latihan, anggap saja saya berada di sebuah cluster Kubernetes yang menggunakan containerd dan memiliki workload dengan definisi seperti berikut ini:
Pada definisi di atas, terdapat 2 Deployment berbeda: nginx dan internal-secret-nginx. Deployment nginx memiliki akses ke /run/containerd/containerd.sock yang ada di host. Bila seandainya saya berhasil mendapatkan shell di Pod nginx, maka secara tidak langsung saya juga bisa melakukan lateral movement untuk mendapatkan shell di Pod internal-secret-nginx.
Metode Low Level
Berbeda dengan Docker yang menggunakan HTTP, containerd menggunakan protokol gRPC dalam komunikasi socket-nya. Oleh sebab itu, saya tidak bisa menggunakan cURL seperti di socket Docker. Cara yang paling gampang untuk menggunakan gRPC adalah membuat kode program client gRPC. Saya akan mulai dengan membuat sebuah proyek Go baru dan memberikan perintah berikut ini untuk menambahkan dependency ke library gRPC:
Saya kemudian membuat kode program berikut ini:
Pada kode program di atas, untuk menggunakan Unix socket di gRPC, saya melewatkan nilai seperti unix:///run/containerd/containerd.sock sebagai argumen untuk grpc.NewClient(). Kubernetes akan menggunakan namespacek8s.io di containerd sehingga saya perlu melewatkan interceptor untuk mengatur nilai namespace. Kemudian,saya memanggil method List dari Containers untuk mendapatkan daftar seluruh container yang ada.
Setelah men-build kode program di atas dan menyalin hasil binary-nya ke dalam Pod (misalnya di-download lewat HTTP), saya akan menemukan hasil eksekusinya yang terlihat seperti berikut ini:
Terlihat bahwa walaupun saya berada di Pod nginx, saya bisa melihat container di Pod lain seperti milik Pod internal-secret-nginx. Saya bahkan bisa melihat container milik Kubernetes di kube-system.
Berkomunikasi dengan socket langsung dari kode program buatan sendiri tidak direkomendasikan karena repetitif dan rentan terhadap kesalahan (kecuali tujuannya adalah mengerjakan exploit tertentu). Saya melakukannya hanya untuk menunjukkan bahwa Unix socket milik container runtime seperti containerd.sock dan docker.sock dapat langsung dipakai untuk mengakses container runtime secara keseluruhan (walaupun kesannya terlihat hanya seperti sebuah file biasa). Cara yang lebih mudah untuk memanggil API containerd adalah dengan menggunakan CLI resmi seperti ctr dan crictl.
ctr
CLI resmi yang dibuat untuk berkomunikasi dengan containerd adalah ctr. Tool ini sudah menjadi bagian dari file rilis containerd. Sebagai contoh, untuk men-download file ini dari dalam shell milik Pod nginx, saya akan memberikan perintah seperti berikut ini:
Setelah file berhasil di-download dan di-extract, saya dapat memberikan perintah seperti berikut ini untuk melihat container yang sedang aktif:
Tampilan dari ctr containers ls terlihat sangat sederhana. Saya tidak punya menemukan pilihan untuk menambahkan kolom lain seperti label untuk melihat nama pod dan namespace dari setiap baris container id. Sebagai gantinya, saya bisa menggunakan perintah ctr containers info untuk mendapat informasi detail setiap container satu per satu dengan menyertakan container id (kolom pertama) seperti pada perintah berikut ini:
Hasil di atas menunjukkan bahwa salah satu Pod internal-secret-nginx dijalankan melalui container dengan ID 2b70c6759a460d05489f81c5e32f5bfb532c1fa2bca62254f45292aac2bf9369. Sampai disini saya bisa melakukan lateral movement dengan mengerjakan perintah pada container tersebut. Namun, saat mencobanya, ctr mengalami kegagalan dalam membuat pipe. Sebagai gantinya, pada langkah berikutnya, saya akan menggunakan crictl.
crictl
Bila ctr adalah tool yang datang dari containerd, maka crictl adalah tool resmi dari Kubernetes untuk berkomunikasi dengan komponen Container Runtime Interface (CRI). Dengan demikian, crictl tidak hanya mendukung containerd, namun juga container runtime lain yang didukung oleh CRI.
Untuk men-downloadcrictl, setelah mendapatkan akses shell ke container yang menjalankan nginx, saya akan mengerjakan perintah berikut ini dari dalam container tersebut:
crictl dapat menampilkan Pod (sama seperti kubectl) dengan perintah seperti berikut ini:
crictl juga dapat menampilkan daftar container yang sedang berjalan dengan perintah seperti:
Terlihat bahwa hasil perintah crictl jauh lebih lengkap bila dibandingkan dengan hasil ctr sebelumnya.
Sekarang, saatnya untuk melakukan lateral movement. Dari Pod nginx, saya akan mengerjakan perintah berikut ini untuk masuk ke container milik internal-secret-nginx:
Ini hampir sama seperti menjalankan kubectl exec -it, hanya saja saya bukan nama Pod yang dilewatkan melainkan container id. Selain mengerjakan shell di container lain, saya juga dapat melihat environment variables di container yang ada karena biasanya nilai secret akan dilewatkan sebagai environment variables.