Kecemasan Pemisahan: Tutorial untuk Mengisolasi Sistem Anda dengan Ruang Nama Linux

Diterbitkan: 2022-03-11

Dengan munculnya alat seperti Docker, Linux Containers, dan lainnya, menjadi sangat mudah untuk mengisolasi proses Linux ke dalam lingkungan sistem kecil mereka sendiri. Hal ini memungkinkan untuk menjalankan berbagai macam aplikasi pada satu mesin Linux nyata dan memastikan tidak ada dua dari mereka yang dapat saling mengganggu, tanpa harus menggunakan mesin virtual. Alat-alat ini telah menjadi keuntungan besar bagi penyedia PaaS. Tapi apa sebenarnya yang terjadi di bawah tenda?

Alat-alat ini bergantung pada sejumlah fitur dan komponen kernel Linux. Beberapa fitur ini diperkenalkan baru-baru ini, sementara yang lain masih mengharuskan Anda untuk menambal kernel itu sendiri. Tetapi salah satu komponen kunci, menggunakan ruang nama Linux, telah menjadi fitur Linux sejak versi 2.6.24 dirilis pada tahun 2008.

Siapapun yang akrab dengan chroot sudah memiliki ide dasar tentang apa yang dapat dilakukan oleh namespace Linux dan bagaimana menggunakan namespace secara umum. Sama seperti chroot memungkinkan proses untuk melihat direktori sewenang-wenang sebagai root dari sistem (terlepas dari sisa proses), ruang nama Linux memungkinkan aspek lain dari sistem operasi untuk dimodifikasi secara independen juga. Ini termasuk pohon proses, antarmuka jaringan, titik pemasangan, sumber daya komunikasi antar-proses dan banyak lagi.

Mengapa Menggunakan Ruang Nama untuk Isolasi Proses?

Dalam komputer pengguna tunggal, lingkungan sistem tunggal mungkin baik-baik saja. Tetapi pada server, di mana Anda ingin menjalankan beberapa layanan, penting untuk keamanan dan stabilitas bahwa layanan terisolasi satu sama lain mungkin. Bayangkan sebuah server menjalankan beberapa layanan, salah satunya dikompromikan oleh penyusup. Dalam kasus seperti itu, penyusup mungkin dapat mengeksploitasi layanan itu dan menuju ke layanan lain, dan bahkan mungkin dapat membahayakan seluruh server. Isolasi namespace dapat menyediakan lingkungan yang aman untuk menghilangkan risiko ini.

Misalnya, dengan menggunakan penspasian nama, Anda dapat dengan aman menjalankan program sewenang-wenang atau tidak dikenal di server Anda. Baru-baru ini, semakin banyak kontes pemrograman dan platform "hackathon", seperti HackerRank, TopCoder, Codeforces, dan banyak lagi. Banyak dari mereka menggunakan saluran otomatis untuk menjalankan dan memvalidasi program yang diajukan oleh para kontestan. Seringkali tidak mungkin untuk mengetahui sebelumnya sifat sebenarnya dari program kontestan, dan beberapa bahkan mungkin mengandung unsur jahat. Dengan menjalankan program-program yang diberi namespace dalam isolasi lengkap dari sistem lainnya, perangkat lunak dapat diuji dan divalidasi tanpa membahayakan mesin lainnya. Demikian pula, layanan integrasi berkelanjutan online, seperti Drone.io, secara otomatis mengambil repositori kode Anda dan menjalankan skrip pengujian di server mereka sendiri. Sekali lagi, isolasi namespace memungkinkan untuk menyediakan layanan ini dengan aman.

Alat penspasian nama seperti Docker juga memungkinkan kontrol yang lebih baik atas penggunaan sumber daya sistem oleh proses, membuat alat tersebut sangat populer untuk digunakan oleh penyedia PaaS. Layanan seperti Heroku dan Google App Engine menggunakan alat tersebut untuk mengisolasi dan menjalankan beberapa aplikasi server web pada perangkat keras yang sama. Alat-alat ini memungkinkan mereka untuk menjalankan setiap aplikasi (yang mungkin telah digunakan oleh salah satu dari sejumlah pengguna yang berbeda) tanpa khawatir salah satu dari mereka menggunakan terlalu banyak sumber daya sistem, atau mengganggu dan/atau bertentangan dengan layanan lain yang digunakan pada mesin yang sama. Dengan isolasi proses seperti itu, bahkan dimungkinkan untuk memiliki tumpukan perangkat lunak dependensi (dan versi) yang sama sekali berbeda untuk setiap lingkungan yang terisolasi!

Jika Anda pernah menggunakan alat seperti Docker, Anda sudah tahu bahwa alat ini mampu mengisolasi proses dalam "wadah" kecil. Menjalankan proses dalam wadah Docker seperti menjalankannya di mesin virtual, hanya wadah ini yang secara signifikan lebih ringan daripada mesin virtual. Mesin virtual biasanya mengemulasi lapisan perangkat keras di atas sistem operasi Anda, dan kemudian menjalankan sistem operasi lain di atasnya. Ini memungkinkan Anda untuk menjalankan proses di dalam mesin virtual, dalam isolasi penuh dari sistem operasi Anda yang sebenarnya. Tapi mesin virtual itu berat! Kontainer Docker, di sisi lain, menggunakan beberapa fitur utama dari sistem operasi Anda yang sebenarnya, termasuk ruang nama, dan memastikan tingkat isolasi yang sama, tetapi tanpa meniru perangkat keras dan menjalankan sistem operasi lain pada mesin yang sama. Ini membuat mereka sangat ringan.

Ruang Nama Proses

Secara historis, kernel Linux telah mempertahankan satu pohon proses. Pohon berisi referensi ke setiap proses yang sedang berjalan dalam hierarki induk-anak. Suatu proses, jika memiliki hak istimewa yang cukup dan memenuhi kondisi tertentu, dapat memeriksa proses lain dengan melampirkan pelacak padanya atau bahkan dapat mematikannya.

Dengan diperkenalkannya ruang nama Linux, menjadi mungkin untuk memiliki beberapa pohon proses "bersarang". Setiap pohon proses dapat memiliki serangkaian proses yang sepenuhnya terisolasi. Ini dapat memastikan bahwa proses yang termasuk dalam satu pohon proses tidak dapat memeriksa atau mematikan - bahkan tidak dapat mengetahui keberadaan - proses di pohon proses saudara atau induk lainnya.

Setiap kali komputer dengan Linux boot, itu dimulai hanya dengan satu proses, dengan pengidentifikasi proses (PID) 1. Proses ini adalah akar dari pohon proses, dan memulai sisa sistem dengan melakukan pekerjaan pemeliharaan yang sesuai dan memulai daemon/layanan yang benar. Semua proses lain dimulai di bawah proses ini di pohon. Namespace PID memungkinkan seseorang untuk memutar pohon baru, dengan proses PID 1-nya sendiri. Proses yang melakukan ini tetap berada di namespace induk, di pohon asli, tetapi menjadikan anak sebagai akar dari pohon prosesnya sendiri.

Dengan isolasi namespace PID, proses di namespace anak tidak memiliki cara untuk mengetahui keberadaan proses induk. Namun, proses di ruang nama induk memiliki tampilan lengkap proses di ruang nama anak, seolah-olah proses tersebut adalah proses lain di ruang nama induk.

Tutorial namespace ini menguraikan pemisahan berbagai pohon proses menggunakan sistem namespace di Linux.

Dimungkinkan untuk membuat kumpulan ruang nama anak bersarang: satu proses memulai proses anak di ruang nama PID baru, dan proses anak itu memunculkan proses lain di ruang nama PID baru, dan seterusnya.

Dengan pengenalan ruang nama PID, satu proses sekarang dapat memiliki beberapa PID yang terkait dengannya, satu untuk setiap ruang nama yang termasuk di dalamnya. Dalam kode sumber Linux, kita dapat melihat bahwa sebuah struct bernama pid , yang digunakan untuk melacak hanya satu PID, sekarang melacak beberapa PID melalui penggunaan sebuah struct bernama upid :

 struct upid { int nr; // the PID value struct pid_namespace *ns; // namespace where this PID is relevant // ... }; struct pid { // ... int level; // number of upids struct upid numbers[0]; // array of upids };

Untuk membuat namespace PID baru, seseorang harus memanggil system call clone() dengan flag khusus CLONE_NEWPID . (C menyediakan pembungkus untuk mengekspos panggilan sistem ini, dan begitu juga banyak bahasa populer lainnya.) Sedangkan ruang nama lain yang dibahas di bawah ini juga dapat dibuat menggunakan panggilan sistem unshare() , ruang nama PID hanya dapat dibuat pada saat yang baru proses muncul menggunakan clone() . Setelah clone() dipanggil dengan flag ini, proses baru segera dimulai di namespace PID baru, di bawah pohon proses baru. Ini dapat ditunjukkan dengan program C sederhana:

 #define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> static char child_stack[1048576]; static int child_fn() { printf("PID: %ld\n", (long)getpid()); return 0; } int main() { pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID | SIGCHLD, NULL); printf("clone() = %ld\n", (long)child_pid); waitpid(child_pid, NULL, 0); return 0; }

Kompilasi dan jalankan program ini dengan hak akses root dan Anda akan melihat output yang menyerupai ini:

 clone() = 5304 PID: 1

PID, seperti yang dicetak dari dalam child_fn , akan menjadi 1 .

Meskipun kode tutorial namespace di atas tidak lebih panjang dari "Halo, dunia" dalam beberapa bahasa, banyak yang telah terjadi di balik layar. Fungsi clone() , seperti yang Anda harapkan, telah membuat proses baru dengan mengkloning proses saat ini dan memulai eksekusi di awal fungsi child_fn() . Namun, saat melakukannya, ia melepaskan proses baru dari pohon proses asli dan membuat pohon proses terpisah untuk proses baru.

Coba ganti fungsi static int child_fn() dengan yang berikut ini, untuk mencetak PID induk dari perspektif proses yang terisolasi:

 static int child_fn() { printf("Parent PID: %ld\n", (long)getppid()); return 0; }

Menjalankan program kali ini menghasilkan output berikut:

 clone() = 11449 Parent PID: 0

Perhatikan bagaimana PID induk dari perspektif proses terisolasi adalah 0, menunjukkan tidak ada induk. Coba jalankan program yang sama lagi, tetapi kali ini, hapus tanda CLONE_NEWPID dari dalam panggilan fungsi clone() :

 pid_t child_pid = clone(child_fn, child_stack+1048576, SIGCHLD, NULL);

Kali ini, Anda akan melihat bahwa PID induk tidak lagi 0:

 clone() = 11561 Parent PID: 11560

Namun, ini hanyalah langkah pertama dalam tutorial kami. Proses ini masih memiliki akses tak terbatas ke sumber daya umum atau bersama lainnya. Misalnya, antarmuka jaringan: jika proses anak yang dibuat di atas mendengarkan pada port 80, itu akan mencegah setiap proses lain pada sistem untuk dapat mendengarkannya.

Ruang Nama Jaringan Linux

Di sinilah namespace jaringan menjadi berguna. Ruang nama jaringan memungkinkan setiap proses ini untuk melihat rangkaian antarmuka jaringan yang sama sekali berbeda. Bahkan antarmuka loopback berbeda untuk setiap namespace jaringan.

Mengisolasi proses ke dalam namespace jaringannya sendiri melibatkan pengenalan flag lain ke panggilan fungsi clone() : CLONE_NEWNET ;

 #define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> static char child_stack[1048576]; static int child_fn() { printf("New `net` Namespace:\n"); system("ip link"); printf("\n\n"); return 0; } int main() { printf("Original `net` Namespace:\n"); system("ip link"); printf("\n\n"); pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID | CLONE_NEWNET | SIGCHLD, NULL); waitpid(child_pid, NULL, 0); return 0; }

Keluaran:

 Original `net` Namespace: 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: enp4s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000 link/ether 00:24:8c:a1:ac:e7 brd ff:ff:ff:ff:ff:ff New `net` Namespace: 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

Apa yang terjadi di sini? Perangkat ethernet fisik enp4s0 milik namespace jaringan global, seperti yang ditunjukkan oleh alat "ip" yang dijalankan dari namespace ini. Namun, antarmuka fisik tidak tersedia di namespace jaringan baru. Selain itu, perangkat loopback aktif di namespace jaringan asli, tetapi "down" di namespace jaringan anak.

Untuk menyediakan antarmuka jaringan yang dapat digunakan di ruang nama anak, perlu untuk menyiapkan antarmuka jaringan "virtual" tambahan yang menjangkau beberapa ruang nama. Setelah selesai, dimungkinkan untuk membuat jembatan Ethernet, dan bahkan merutekan paket di antara ruang nama. Akhirnya, untuk membuat semuanya berfungsi, "proses perutean" harus dijalankan di ruang nama jaringan global untuk menerima lalu lintas dari antarmuka fisik, dan merutekannya melalui antarmuka virtual yang sesuai ke ruang nama jaringan anak yang benar. Mungkin Anda dapat melihat mengapa alat seperti Docker, yang melakukan semua pekerjaan berat ini untuk Anda, sangat populer!

Ruang nama jaringan Linux terdiri dari proses perutean ke beberapa ruang nama bersih anak.

Untuk melakukannya secara manual, Anda dapat membuat sepasang koneksi Ethernet virtual antara parent dan namespace anak dengan menjalankan satu perintah dari namespace induk:

 ip link add name veth0 type veth peer name veth1 netns <pid>

Di sini, <pid> harus diganti dengan ID proses dari proses di ruang nama anak seperti yang diamati oleh induknya. Menjalankan perintah ini membuat koneksi seperti pipa antara dua ruang nama ini. Namespace induk mempertahankan perangkat veth0 , dan meneruskan perangkat veth1 ke namespace anak. Apa pun yang masuk ke salah satu ujung, keluar melalui ujung yang lain, seperti yang Anda harapkan dari koneksi Ethernet nyata antara dua node nyata. Oleh karena itu, kedua sisi koneksi Ethernet virtual ini harus diberi alamat IP.

Gunung Namespace

Linux juga memelihara struktur data untuk semua titik mount sistem. Ini mencakup informasi seperti partisi disk apa yang dipasang, di mana mereka dipasang, apakah hanya dapat dibaca, dan lain-lain. Dengan ruang nama Linux, struktur data ini dapat dikloning, sehingga proses di bawah ruang nama yang berbeda dapat mengubah mountpoint tanpa mempengaruhi satu sama lain.

Membuat namespace mount terpisah memiliki efek yang mirip dengan melakukan chroot() . chroot() bagus, tetapi tidak menyediakan isolasi lengkap, dan efeknya terbatas pada titik mount root saja. Membuat namespace mount terpisah memungkinkan setiap proses yang terisolasi ini memiliki pandangan yang sama sekali berbeda dari struktur mountpoint keseluruhan sistem dari yang asli. Ini memungkinkan Anda untuk memiliki root yang berbeda untuk setiap proses yang terisolasi, serta titik mount lain yang khusus untuk proses tersebut. Digunakan dengan hati-hati sesuai tutorial ini, Anda dapat menghindari memaparkan informasi apa pun tentang sistem yang mendasarinya.

Mempelajari cara menggunakan namespace dengan benar memiliki banyak manfaat seperti yang dijelaskan dalam tutorial namespace ini.

Bendera clone() yang diperlukan untuk mencapai ini adalah CLONE_NEWNS :

 clone(child_fn, child_stack+1048576, CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | SIGCHLD, NULL)

Awalnya, proses anak melihat titik mount yang sama persis seperti proses induknya. Namun, berada di bawah mount namespace baru, proses anak dapat me-mount atau meng-unmount endpoint apa pun yang diinginkannya, dan perubahan tidak akan memengaruhi namespace induknya, atau namespace mount lainnya di seluruh sistem. Misalnya, jika proses induk memiliki partisi disk tertentu yang dipasang di root, proses yang terisolasi akan melihat partisi disk yang sama persis dipasang di root di awal. Tetapi manfaat dari mengisolasi mount namespace terlihat jelas ketika proses yang terisolasi mencoba untuk mengubah partisi root menjadi sesuatu yang lain, karena perubahan hanya akan mempengaruhi mount namespace yang terisolasi.

Menariknya, ini sebenarnya merupakan ide yang buruk untuk menelurkan proses anak target secara langsung dengan flag CLONE_NEWNS . Pendekatan yang lebih baik adalah memulai proses "init" khusus dengan flag CLONE_NEWNS , minta proses "init" itu mengubah "/", "/ proc", "/ dev" atau titik mount lainnya sesuai keinginan, lalu mulai proses target . Ini dibahas sedikit lebih detail di akhir tutorial namespace ini.

Namespace lainnya

Ada ruang nama lain tempat proses ini dapat diisolasi, yaitu pengguna, IPC, dan UTS. Ruang nama pengguna memungkinkan proses memiliki hak akses root di dalam ruang nama, tanpa memberikannya akses ke proses di luar ruang nama. Mengisolasi proses dengan namespace IPC memberinya sumber daya komunikasi antarprosesnya sendiri, misalnya, pesan System V IPC dan POSIX. Namespace UTS mengisolasi dua pengidentifikasi khusus dari sistem: nodename dan domainname .

Contoh cepat untuk menunjukkan bagaimana namespace UTS diisolasi ditunjukkan di bawah ini:

 #define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/utsname.h> #include <sys/wait.h> #include <unistd.h> static char child_stack[1048576]; static void print_nodename() { struct utsname utsname; uname(&utsname); printf("%s\n", utsname.nodename); } static int child_fn() { printf("New UTS namespace nodename: "); print_nodename(); printf("Changing nodename inside new UTS namespace\n"); sethostname("GLaDOS", 6); printf("New UTS namespace nodename: "); print_nodename(); return 0; } int main() { printf("Original UTS namespace nodename: "); print_nodename(); pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWUTS | SIGCHLD, NULL); sleep(1); printf("Original UTS namespace nodename: "); print_nodename(); waitpid(child_pid, NULL, 0); return 0; }

Program ini menghasilkan output berikut:

 Original UTS namespace nodename: XT New UTS namespace nodename: XT Changing nodename inside new UTS namespace New UTS namespace nodename: GLaDOS Original UTS namespace nodename: XT

Di sini, child_fn() mencetak nodename , mengubahnya menjadi sesuatu yang lain, dan mencetaknya lagi. Secara alami, perubahan hanya terjadi di dalam namespace UTS yang baru.

Informasi lebih lanjut tentang apa yang disediakan dan diisolasi oleh semua namespace dapat ditemukan di tutorial di sini

Komunikasi Lintas Nama Ruang

Seringkali perlu untuk membangun semacam komunikasi antara orang tua dan namespace anak. Ini mungkin untuk melakukan pekerjaan konfigurasi dalam lingkungan yang terisolasi, atau bisa juga untuk mempertahankan kemampuan untuk mengintip kondisi lingkungan itu dari luar. Salah satu cara untuk melakukannya adalah dengan menjaga daemon SSH tetap berjalan di dalam lingkungan tersebut. Anda dapat memiliki daemon SSH terpisah di dalam setiap namespace jaringan. Namun, menjalankan beberapa daemon SSH menggunakan banyak sumber daya berharga seperti memori. Di sinilah memiliki proses "init" khusus terbukti menjadi ide yang bagus lagi.

Proses "init" dapat membangun saluran komunikasi antara namespace induk dan namespace anak. Saluran ini dapat didasarkan pada soket UNIX atau bahkan dapat menggunakan TCP. Untuk membuat soket UNIX yang mencakup dua ruang nama pemasangan yang berbeda, Anda harus terlebih dahulu membuat proses anak, kemudian membuat soket UNIX, dan kemudian mengisolasi anak ke dalam ruang nama pemasangan yang terpisah. Tetapi bagaimana kita bisa membuat prosesnya terlebih dahulu, dan mengisolasinya nanti? Linux menyediakan unshare() . Panggilan sistem khusus ini memungkinkan proses untuk mengisolasi dirinya dari namespace asli, alih-alih membuat induk mengisolasi anak di tempat pertama. Misalnya, kode berikut memiliki efek yang sama persis dengan kode yang disebutkan sebelumnya di bagian ruang nama jaringan:

 #define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> static char child_stack[1048576]; static int child_fn() { // calling unshare() from inside the init process lets you create a new namespace after a new process has been spawned unshare(CLONE_NEWNET); printf("New `net` Namespace:\n"); system("ip link"); printf("\n\n"); return 0; } int main() { printf("Original `net` Namespace:\n"); system("ip link"); printf("\n\n"); pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID | SIGCHLD, NULL); waitpid(child_pid, NULL, 0); return 0; }

Dan karena proses "init" adalah sesuatu yang telah Anda rancang, Anda dapat membuatnya melakukan semua pekerjaan yang diperlukan terlebih dahulu, dan kemudian mengisolasi dirinya dari sisa sistem sebelum mengeksekusi anak target.

Kesimpulan

Tutorial ini hanyalah gambaran umum tentang cara menggunakan namespace di Linux. Ini akan memberi Anda ide dasar tentang bagaimana pengembang Linux mungkin mulai menerapkan isolasi sistem, bagian integral dari arsitektur alat seperti Docker atau Linux Containers. Dalam kebanyakan kasus, akan lebih baik untuk hanya menggunakan salah satu alat yang ada, yang sudah terkenal dan diuji. Tetapi dalam beberapa kasus, mungkin masuk akal untuk memiliki mekanisme isolasi proses Anda sendiri yang disesuaikan, dan dalam hal ini, tutorial namespace ini akan sangat membantu Anda.

Ada lebih banyak hal yang terjadi di bawah tenda daripada yang saya bahas dalam artikel ini, dan ada lebih banyak cara Anda mungkin ingin membatasi proses target Anda untuk keamanan dan isolasi tambahan. Tapi, semoga, ini bisa menjadi titik awal yang berguna bagi seseorang yang tertarik untuk mengetahui lebih banyak tentang cara kerja isolasi namespace dengan Linux.