Ayırma Kaygısı: Sisteminizi Linux Ad Alanlarıyla Yalıtmak İçin Bir Eğitim

Yayınlanan: 2022-03-11

Docker, Linux Containers ve diğerleri gibi araçların ortaya çıkmasıyla birlikte, Linux işlemlerini kendi küçük sistem ortamlarında izole etmek çok kolay hale geldi. Bu, tek bir gerçek Linux makinesinde bir dizi uygulamayı çalıştırmayı mümkün kılar ve sanal makineleri kullanmaya başvurmak zorunda kalmadan ikisinin birbiriyle etkileşime girmemesini sağlar. Bu araçlar, PaaS sağlayıcıları için büyük bir nimet oldu. Ama kaputun altında tam olarak ne oluyor?

Bu araçlar, Linux çekirdeğinin bir dizi özelliğine ve bileşenine dayanır. Bu özelliklerden bazıları oldukça yakın zamanda tanıtıldı, diğerleri ise hala çekirdeğin kendisine yama yapmanızı gerektiriyor. Ancak, Linux ad alanlarını kullanan temel bileşenlerden biri, 2008'de 2.6.24 sürümünün yayınlanmasından bu yana Linux'un bir özelliği olmuştur.

chroot aşina olan herkes, Linux ad alanlarının neler yapabileceği ve genel olarak ad alanının nasıl kullanılacağı hakkında zaten temel bir fikre sahiptir. Tıpkı chroot süreçlerin herhangi bir rastgele dizini sistemin kökü olarak (işlemlerin geri kalanından bağımsız olarak) görmesine izin vermesi gibi, Linux ad alanları da işletim sisteminin diğer yönlerinin bağımsız olarak değiştirilmesine izin verir. Buna süreç ağacı, ağ arabirimleri, bağlama noktaları, süreçler arası iletişim kaynakları ve daha fazlası dahildir.

İşlem İzolasyonu için Neden Ad Alanlarını Kullanmalısınız?

Tek kullanıcılı bir bilgisayarda, tek bir sistem ortamı uygun olabilir. Ancak birden fazla hizmeti çalıştırmak istediğiniz bir sunucuda, hizmetlerin mümkün olduğunca birbirinden izole olması güvenlik ve istikrar açısından çok önemlidir. Birden çok hizmet çalıştıran bir sunucu düşünün ve bunlardan biri bir davetsiz misafir tarafından ele geçirilir. Böyle bir durumda, davetsiz misafir bu hizmetten yararlanabilir ve diğer hizmetlere gidebilir ve hatta tüm sunucuyu tehlikeye atabilir. Ad alanı yalıtımı, bu riski ortadan kaldırmak için güvenli bir ortam sağlayabilir.

Örneğin, ad alanını kullanarak, sunucunuzda rastgele veya bilinmeyen programları güvenle yürütmek mümkündür. Son zamanlarda, HackerRank, TopCoder, Codeforces ve daha pek çok şey gibi artan sayıda programlama yarışması ve "hackathon" platformu var. Birçoğu, yarışmacılar tarafından sunulan programları çalıştırmak ve doğrulamak için otomatik ardışık düzenleri kullanır. Yarışmacıların programlarının gerçek doğasını önceden bilmek çoğu zaman imkansızdır ve hatta bazıları kötü niyetli unsurlar içerebilir. Bu programları sistemin geri kalanından tamamen izole edilmiş bir şekilde çalıştırarak, makinenin geri kalanını riske atmadan yazılım test edilebilir ve doğrulanabilir. Benzer şekilde, Drone.io gibi çevrimiçi sürekli entegrasyon hizmetleri, kod deponuzu otomatik olarak getirir ve test komut dosyalarını kendi sunucularında yürütür. Yine, ad alanı yalıtımı, bu hizmetleri güvenli bir şekilde sağlamayı mümkün kılan şeydir.

Docker gibi ad alanı araçları, süreçlerin sistem kaynaklarını kullanımı üzerinde daha iyi kontrole izin vererek, bu tür araçları PaaS sağlayıcıları tarafından son derece popüler hale getirir. Heroku ve Google App Engine gibi hizmetler, aynı gerçek donanım üzerinde birden çok web sunucusu uygulamasını yalıtmak ve çalıştırmak için bu tür araçları kullanır. Bu araçlar, her bir uygulamayı (birkaç farklı kullanıcı tarafından dağıtılmış olabilir) bunlardan birinin çok fazla sistem kaynağı kullanması veya aynı makinede konuşlandırılmış diğer hizmetlere müdahale etmesi ve/veya bunlarla çakışması konusunda endişelenmeden çalıştırmalarına izin verir. Böyle bir süreç izolasyonu ile, her izole edilmiş ortam için tamamen farklı bağımlılık yazılımları (ve versiyonları) yığınlarına sahip olmak bile mümkündür!

Docker gibi araçlar kullandıysanız, bu araçların küçük “konteynerlerdeki” süreçleri izole edebildiğini zaten biliyorsunuzdur. İşlemleri Docker kapsayıcılarında çalıştırmak, onları sanal makinelerde çalıştırmak gibidir, yalnızca bu kapsayıcılar sanal makinelerden önemli ölçüde daha hafiftir. Bir sanal makine, genellikle işletim sisteminizin üzerinde bir donanım katmanını öykünür ve ardından bunun üzerinde başka bir işletim sistemini çalıştırır. Bu, işlemleri gerçek işletim sisteminizden tamamen izole bir şekilde sanal bir makine içinde çalıştırmanıza olanak tanır. Ancak sanal makineler ağırdır! Öte yandan Docker kapsayıcıları, gerçek işletim sisteminizin ad alanları dahil olmak üzere bazı temel özelliklerini kullanır ve benzer bir yalıtım düzeyi sağlar, ancak donanımı taklit etmeden ve aynı makinede başka bir işletim sistemi çalıştırmadan. Bu onları çok hafif yapar.

İşlem Ad Alanı

Tarihsel olarak, Linux çekirdeği tek bir işlem ağacını korumuştur. Ağaç, şu anda bir üst-alt hiyerarşisinde çalışan her işleme bir başvuru içerir. Yeterli ayrıcalıklara sahip olduğu ve belirli koşulları yerine getirdiği göz önüne alındığında, bir süreç, kendisine bir izleyici ekleyerek başka bir süreci inceleyebilir veya hatta onu öldürebilir.

Linux ad alanlarının tanıtılmasıyla, birden çok "iç içe" işlem ağacına sahip olmak mümkün hale geldi. Her süreç ağacı, tamamen izole edilmiş bir süreç kümesine sahip olabilir. Bu, bir süreç ağacına ait süreçlerin diğer kardeş ya da üst süreç ağaçlarındaki süreçleri denetleyememesini ya da öldürmemesini - aslında varlığını bile bilmemesini - garanti edebilir.

Linux'lu bir bilgisayar her başlatıldığında, işlem tanımlayıcısı (PID) 1 ile yalnızca bir işlemle başlar. Bu işlem, işlem ağacının köküdür ve uygun bakım çalışmasını gerçekleştirerek ve başlatarak sistemin geri kalanını başlatır. doğru cinler/hizmetler. Diğer tüm işlemler ağaçta bu işlemin altında başlar. PID ad alanı, kişinin kendi PID 1 işlemiyle yeni bir ağaç döndürmesine izin verir. Bunu yapan işlem, orijinal ağaçta üst ad alanında kalır, ancak alt öğeyi kendi işlem ağacının kökü yapar.

PID ad alanı yalıtımıyla, alt ad alanındaki süreçlerin üst sürecin varlığını bilmesinin hiçbir yolu yoktur. Ancak, üst ad alanındaki işlemler, sanki üst ad alanındaki herhangi bir işlemmiş gibi, alt ad alanındaki işlemlerin eksiksiz bir görünümüne sahiptir.

Bu ad alanı öğreticisi, Linux'ta ad alanı sistemlerini kullanan çeşitli işlem ağaçlarının ayrılmasını özetlemektedir.

İç içe bir alt ad alanı kümesi oluşturmak mümkündür: bir işlem, yeni bir PID ad alanında bir alt işlem başlatır ve bu alt işlem, yeni bir PID ad alanında başka bir işlem üretir ve bu böyle devam eder.

PID ad alanlarının tanıtılmasıyla, artık tek bir işlem, altına düştüğü her ad alanı için bir tane olmak üzere kendisiyle ilişkilendirilmiş birden çok PID'ye sahip olabilir. Linux kaynak kodunda, sadece tek bir PID'yi takip etmek için kullanılan pid adlı bir yapının, şimdi upid adlı bir yapı kullanarak birden fazla PID'yi izlediğini görebiliriz:

 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 };

Yeni bir PID ad alanı oluşturmak için, clone() sistem çağrısını CLONE_NEWPID özel bayrağıyla çağırmak gerekir. (C, bu sistem çağrısını ortaya çıkarmak için bir sarmalayıcı sağlar ve diğer birçok popüler dil de öyle.) Aşağıda tartışılan diğer ad alanları, unshare() sistem çağrısı kullanılarak da oluşturulabilirken, bir PID ad alanı yalnızca yeni bir zamanda oluşturulabilir. süreç clone() kullanılarak oluşturulur. Bu bayrakla clone() çağrıldığında, yeni işlem hemen yeni bir işlem ağacı altında yeni bir PID ad alanında başlar. Bu basit bir C programı ile gösterilebilir:

 #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; }

Bu programı kök ayrıcalıklarıyla derleyin ve çalıştırın; buna benzer bir çıktı göreceksiniz:

 clone() = 5304 PID: 1

child_fn içinden yazdırıldığı şekliyle PID, 1 olacaktır.

Yukarıdaki ad alanı öğretici kodu, bazı dillerde "Merhaba, dünya"dan çok daha uzun olmasa da, sahne arkasında çok şey oldu. clone() işlevi, beklediğiniz gibi, mevcut olanı klonlayarak yeni bir süreç yarattı ve child_fn() işlevinin başlangıcında yürütmeye başladı. Ancak bunu yaparken yeni süreci orijinal süreç ağacından ayırdı ve yeni süreç için ayrı bir süreç ağacı oluşturdu.

Yalıtılmış işlemin bakış açısından üst PID'yi yazdırmak için static int child_fn() işlevini aşağıdakiyle değiştirmeyi deneyin:

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

Programı bu sefer çalıştırmak aşağıdaki çıktıyı verir:

 clone() = 11449 Parent PID: 0

Yalıtılmış sürecin perspektifinden ebeveyn PID'sinin 0 olduğuna dikkat edin, bu da ebeveyn olmadığını gösterir. Aynı programı yeniden çalıştırmayı deneyin, ancak bu sefer CLONE_NEWPID bayrağını clone() işlev çağrısından kaldırın:

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

Bu sefer, ana PID'nin artık 0 olmadığını fark edeceksiniz:

 clone() = 11561 Parent PID: 11560

Ancak bu, öğreticimizin yalnızca ilk adımıdır. Bu süreçler, diğer ortak veya paylaşılan kaynaklara hala sınırsız erişime sahiptir. Örneğin, ağ arabirimi: yukarıda oluşturulan alt süreç 80 numaralı bağlantı noktasını dinleseydi, sistemdeki diğer tüm işlemlerin onu dinlemesini engellerdi.

Linux Ağ Ad Alanı

Burası bir ağ ad alanının kullanışlı hale geldiği yerdir. Bir ağ ad alanı, bu işlemlerin her birinin tamamen farklı bir ağ arabirimi kümesi görmesine olanak tanır. Her ağ ad alanı için geri döngü arabirimi bile farklıdır.

Bir işlemi kendi ağ ad alanına ayırmak, clone() işlev çağrısına başka bir bayrak eklemeyi içerir: 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; }

Çıktı:

 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

Burada neler oluyor? Fiziksel ethernet aygıtı enp4s0 , bu ad alanından çalıştırılan “ip” aracı tarafından belirtildiği gibi, küresel ağ ad alanına aittir. Ancak, yeni ağ ad alanında fiziksel arabirim mevcut değildir. Ayrıca, geri döngü aygıtı orijinal ağ ad alanında etkindir, ancak alt ağ ad alanında "aşağı"dır.

Alt ad alanında kullanılabilir bir ağ arabirimi sağlamak için, birden çok ad alanına yayılan ek "sanal" ağ arabirimleri kurmak gerekir. Bu yapıldıktan sonra, Ethernet köprüleri oluşturmak ve hatta paketleri ad alanları arasında yönlendirmek mümkündür. Son olarak, her şeyin çalışması için, fiziksel arabirimden trafik almak ve uygun sanal arabirimler aracılığıyla doğru alt ağ ad alanlarına yönlendirmek için küresel ağ ad alanında bir "yönlendirme işlemi" çalışıyor olmalıdır. Belki de Docker gibi sizin için tüm bu ağır yükleri kaldıran araçların neden bu kadar popüler olduğunu anlamışsınızdır!

Linux ağ ad alanı, birden çok alt ağ ad alanına bir yönlendirme işleminden oluşur.

Bunu elle yapmak için, üst ad alanından tek bir komut çalıştırarak bir üst öğe ve alt ad alanı arasında bir çift sanal Ethernet bağlantısı oluşturabilirsiniz:

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

Burada <pid> , ebeveyn tarafından gözlemlendiği gibi alt ad alanındaki işlemin işlem kimliği ile değiştirilmelidir. Bu komutu çalıştırmak, bu iki ad alanı arasında boru benzeri bir bağlantı kurar. Üst ad alanı veth0 aygıtını korur ve veth0 aygıtını alt ad veth1 . İki gerçek düğüm arasındaki gerçek bir Ethernet bağlantısından beklediğiniz gibi, uçlardan birine giren herhangi bir şey diğer uçtan çıkar. Buna göre, bu sanal Ethernet bağlantısının her iki tarafına da IP adresleri atanmalıdır.

Ad Alanı Ekle

Linux ayrıca sistemin tüm bağlama noktaları için bir veri yapısı tutar. Hangi disk bölümlerinin monte edildiği, nereye monte edildikleri, salt okunur olup olmadıkları gibi bilgileri içerir. Linux ad alanları ile, bu veri yapısı klonlanabilir, böylece farklı ad alanları altındaki işlemler, birbirini etkilemeden bağlama noktalarını değiştirebilir.

Ayrı mount ad alanı oluşturmak, chroot() yapmaya benzer bir etkiye sahiptir. chroot() iyidir, ancak tam yalıtım sağlamaz ve etkileri yalnızca kök bağlama noktasıyla sınırlıdır. Ayrı bir bağlama ad alanı oluşturmak, bu yalıtılmış işlemlerin her birinin, tüm sistemin bağlama noktası yapısının orijinalinden tamamen farklı bir görünümüne sahip olmasını sağlar. Bu, her yalıtılmış işlem için farklı bir köke ve bu işlemlere özgü diğer bağlama noktalarına sahip olmanızı sağlar. Bu eğiticiye göre dikkatli kullanıldığında, temel sistem hakkında herhangi bir bilginin ifşa edilmesini önleyebilirsiniz.

Bu ad alanı öğreticisinde özetlendiği gibi, ad alanını doğru şekilde kullanmayı öğrenmenin birçok faydası vardır.

Bunu başarmak için gerekli olan clone() bayrağı CLONE_NEWNS :

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

Başlangıçta, alt süreç, üst süreciyle tam olarak aynı bağlama noktalarını görür. Ancak, yeni bir bağlama ad alanı altında olduğundan, alt süreç istediği uç noktaları bağlayabilir veya bağlantısını kaldırabilir ve değişiklik, üst öğesinin ad alanını veya tüm sistemdeki diğer herhangi bir bağlama ad alanını etkilemez. Örneğin, ana işlemin köke monte edilmiş belirli bir disk bölümü varsa, yalıtılmış işlem başlangıçta köke monte edilmiş tam olarak aynı disk bölümünü görecektir. Ancak, mount ad alanını ayırmanın yararı, yalıtılmış işlem kök bölümü başka bir şeye değiştirmeye çalıştığında, değişiklik yalnızca yalıtılmış mount ad alanını etkileyeceğinden açıktır.

İlginç bir şekilde, bu aslında hedef alt süreci doğrudan CLONE_NEWNS bayrağıyla oluşturmayı kötü bir fikir haline getiriyor. Daha iyi bir yaklaşım, CLONE_NEWNS bayrağıyla özel bir "init" işlemi başlatmak, "init" işleminin "/", "/proc", "/dev" veya diğer bağlama noktalarını istediğiniz gibi değiştirmesini sağlamak ve ardından hedef işlemi başlatmaktır. . Bu, bu ad alanı öğreticisinin sonuna yakın bir yerde biraz daha ayrıntılı olarak tartışılmaktadır.

Diğer Ad Alanları

Kullanıcı, IPC ve UTS gibi bu süreçlerin izole edilebileceği başka ad alanları da vardır. Kullanıcı ad alanı, bir işlemin ad alanı dışındaki işlemlere erişim izni vermeden ad alanı içinde kök ayrıcalıklarına sahip olmasına izin verir. Bir işlemi IPC ad alanı tarafından yalıtmak, ona kendi süreçler arası iletişim kaynaklarını verir, örneğin System V IPC ve POSIX mesajları. UTS ad alanı, sistemin iki özel tanımlayıcısını yalıtır: nodename ve domainname .

UTS ad alanının nasıl izole edildiğini gösteren hızlı bir örnek aşağıda gösterilmiştir:

 #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; }

Bu program aşağıdaki çıktıyı verir:

 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

Burada child_fn() nodename , başka bir şeyle değiştirir ve yeniden yazdırır. Doğal olarak, değişiklik yalnızca yeni UTS ad alanı içinde gerçekleşir.

Tüm ad alanlarının ne sağladığı ve yalıttığı hakkında daha fazla bilgiyi buradaki öğreticide bulabilirsiniz.

Ad Alanı Arası İletişim

Genellikle ebeveyn ve alt ad alanı arasında bir tür iletişim kurmak gerekir. Bu, yalıtılmış bir ortamda yapılandırma çalışması yapmak için olabilir veya basitçe o ortamın durumuna dışarıdan bakma yeteneğini korumak olabilir. Bunu yapmanın bir yolu, o ortamda bir SSH arka plan programının çalışmasını sağlamaktır. Her ağ ad alanı içinde ayrı bir SSH arka plan programı olabilir. Ancak, birden fazla SSH arka plan programının çalıştırılması, bellek gibi birçok değerli kaynağı kullanır. İşte burada özel bir "init" işlemine sahip olmak yine iyi bir fikir olduğunu kanıtlıyor.

"init" işlemi, üst ad alanı ile alt ad alanı arasında bir iletişim kanalı kurabilir. Bu kanal UNIX soketlerine dayalı olabilir veya hatta TCP kullanabilir. İki farklı mount ad alanına yayılan bir UNIX soketi oluşturmak için önce alt işlemi oluşturmanız, ardından UNIX soketini oluşturmanız ve ardından alt öğeyi ayrı bir mount ad alanına ayırmanız gerekir. Ama önce süreci nasıl oluşturup daha sonra izole edebiliriz? Linux, unshare() sağlar. Bu özel sistem çağrısı, ebeveynin çocuğu ilk etapta izole etmesi yerine, bir işlemin kendisini orijinal ad alanından izole etmesine izin verir. Örneğin, aşağıdaki kod, ağ ad alanı bölümünde daha önce bahsedilen kodla tamamen aynı etkiye sahiptir:

 #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; }

Ve "init" süreci sizin tasarladığınız bir şey olduğundan, önce gerekli tüm işleri yapmasını sağlayabilir ve ardından hedef çocuğu çalıştırmadan önce kendisini sistemin geri kalanından izole edebilirsiniz.

Çözüm

Bu öğretici, Linux'ta ad alanlarının nasıl kullanılacağına ilişkin yalnızca bir genel bakıştır. Bir Linux geliştiricisinin, Docker veya Linux Containers gibi araç mimarisinin ayrılmaz bir parçası olan sistem yalıtımını uygulamaya nasıl başlayabileceğine dair temel bir fikir vermelidir. Çoğu durumda, zaten iyi bilinen ve test edilmiş olan bu mevcut araçlardan birini kullanmak en iyisidir. Ancak bazı durumlarda, kendi özelleştirilmiş süreç izolasyon mekanizmanıza sahip olmak mantıklı olabilir ve bu durumda, bu ad alanı öğreticisi size çok yardımcı olacaktır.

Kaputun altında bu makalede ele aldığımdan çok daha fazlası var ve daha fazla güvenlik ve izolasyon için hedef süreçlerinizi sınırlamak isteyebileceğiniz daha fazla yol var. Ancak, umarım bu, Linux ile ad alanı yalıtımının gerçekten nasıl çalıştığı hakkında daha fazla bilgi edinmek isteyen biri için yararlı bir başlangıç ​​noktası olabilir.