我們接著上篇文章繼續(xù)討論,通常情況下我們不希望,也不允許容器和宿主機看到相同的文件系統(tǒng)已亥,那樣的話多個容器實例和宿主機會共享相同的文件系統(tǒng),隔離性就不存在了来屠,因此我們一般情況下通過給每個容器創(chuàng)建獨享的mount命名空間來實現文件系統(tǒng)的隔離虑椎。
首先我們來通過例子看看如何賦予進程獨享的mount命名空間:在自己的機器上運行sudo unshare --mount sh來啟動一個新的sh進程,接著創(chuàng)建文件夾source俱笛,并在文件夾中創(chuàng)建文件HELLO捆姜;然后我們再創(chuàng)建另外一個文件件target,并且通過命令mount --bind source target來將source文件夾bind到target文件夾迎膜,如果我們在target文件夾中運行l(wèi)s -l命令泥技,就可以看到source文件夾中的文件HELLO。如下是在筆者機器上的輸出:
vagrant@vagrant:~$ sudo unshare --mount sh
$ mkdir source
$ touch source/HELLO
$ ls source
HELLO
$ mkdir target
$ ls target
$ mount --bind source target
$ ls target
HELLO
上邊的輸出符合預期磕仅,我們接著通過命令findmnt來看看進程sh中具體有哪些mount珊豹,命令運行后會輸出結果簸呈,你可以看到如下這段輸出,也就是我們運行mount命令將源文件夾source和目標文件夾target關聯在一起:
$ findmnt target
TARGET SOURCE FSTYPE OPTIONS
/home/vagrant/target
? ? ? ? /dev/mapper/vagrant--vg-root[/home/vagrant/source]
? ? ? ? ? ? ? ? ext4 rw,relatime,errors=remount-ro,data=ordered
讀者可能會想知道店茶,這個bind是否從宿主機可見蜕便。我們如果在宿主機上運行相同的命令findmnt,你會發(fā)現根本看不到上邊的這個mount信息贩幻。我們剛才在sh的進程中繼續(xù)運行命令findmnt轿腺,這次不帶任何參數,你會發(fā)現會返回很長的一個mount列表丛楚。如果進程具備隔離性吃溅,是不應該看到宿主機上的全部mount信息的,這顯然是有問題的鸯檬。
如果你還記得我們討論PID命名空間的情況嗎,如果只是通過unmount --pid來把進程加入到一個新的PID命名空間中螺垢,我們是無法阻止容器看到宿主機的所有進程信息喧务,因為容器進程中運行ps的時候,其實是請求內核讀取/proc文件夾枉圃,而對于mount命名空間類似功茴,當我們執(zhí)行findmnt命令的時候,內核讀取文件夾/proc/<PID>/mounts文件并返回孽亲。
當我們創(chuàng)建進程的時為進程分配了新的mount命名空間坎穿,但是在進程中運行findmnt的時候,使用的仍然是宿主機的/proc文件夾返劲,因此返回的mount信息中包含了進程創(chuàng)建時間點之前宿主機上所有的mount信息玲昧,我們可以通過cat進程ID對應的文件來驗證:cat /proc/<PID>/mounts。
那么我們如何能讓新創(chuàng)建的進程有自己專屬的文件系統(tǒng)呢篮绿?我們可以通過:1孵延,為容器進程創(chuàng)建新的mount命名空間;2亲配,為進程設置新的root文件系統(tǒng)尘应;3,為進程創(chuàng)建新的proc mount吼虎,如下邊在筆者的機器運行所示:
vagrant@vagrant:~$ sudo unshare --mount chroot alpine sh
/ $ mount -t proc proc proc
/ $ mount
proc on /proc type proc (rw,relatime)
/ $ mkdir source
/ $ touch source/HELLO
/ $ mkdir target
/ $ mount --bind source target
/ $ mount
proc on /proc type proc (rw,relatime)
/dev/sda1 on /target type ext4 (rw,relatime,data=ordered)
熟悉docker的同學一定有過將宿主機上的某個文件夾掛載到容器中的經驗犬钢,比如我們要在容器中運行某些應用程序或者讀取需要的數據,命令docker run -v <宿主機目錄> : <容器目錄> ..., 本質上當容器的root文件夾系統(tǒng)被加載后思灰,會立即創(chuàng)建容器上的目標目錄玷犹,然后宿主機上的目錄被bind到容器中的目錄上,由于每個容器都有自己的mount命名空間官辈,因此通過這種方式掛載到容器中的目錄箱舞,對其他容器不可見遍坟。
有了mount命名空間的知識后,我們繼續(xù)討論Network命名空間晴股。網絡(Network)命名空間賦予容器專屬的網絡設備接口和路由表愿伴。我們可以通過命令行工具lsns來查看機器上的network命名空間。進程啟動可以通過--net來創(chuàng)建新的命名空間电湘,如下邊命令的輸出所示:
vagrant@vagrant:~$ sudo lsns -t net
NS TYPE NPROCS PID USER NETNSID NSFS COMMAND
4026531992 net? 93? ? 1? ? root? ? ? unassigned /sbin/init
vagrant@vagrant:~$ sudo unshare --net bash
root@vagrant:~$ lsns -t net
NS TYPE NPROCS PID USER NETNSID NSFS COMMAND
4026531992 net 92 1 root unassigned /sbin/init
4026532192 net 2 28586 root unassigned bash
當進程運行在專屬的network命名空間隔节,如果不做任何配置,進程只能看到回路接口(lo)寂呛。我們可以通過ip a來驗證怎诫,如下是在筆者機器上的輸出:
vagrant@vagrant:~$ sudo unshare --net bash
root@myhost:~$ ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
? ? link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
只有l(wèi)oopback回路地址也沒有啥用,最簡單的容器之間進行相互通信都無法實現贷痪。為了讓容器能夠和外界連接幻妓,我們需要給容器進程創(chuàng)建virtual Ethernet網絡接口,或者嚴格來說劫拢,我們其實創(chuàng)建了一對虛擬以太網接口肉津。
虛擬以太網接口人如其名,就如同辦公室中的電纜一樣舱沧,將容器的網絡命名空間和宿主機上的默認網絡命名空間連接了起來妹沙。我們在保持前邊bash進程的前提下,在宿主機上的另外一個控制臺窗口上運行命令:root@vagrant:~$ ip link add ve1 netns 32586 type veth peer name ve2 netns 1 來為進程bash創(chuàng)建一對虛擬以太網接口(進程id是32586)熟吏,其中:
- ip link add命令告訴宿主機我們要增加一個link
- ve1是虛擬以太網這根電纜的容器進程端的名字
- netns 32586表示ve1這一端插入到進程32586這個進程的網絡命名空間中
- type veth表示類型距糖,veth表示虛擬以太網
- peer name ve2表示虛擬以太網電纜另外一端的名稱為ve2
- netns 1表示ve2插入到進程號1的網絡命名空間
上邊的命令運行成功后,我們就可以成功的在容器進程中看到ve1(容器端的虛擬以太網接口)牵寺,如下是在筆者機器上啟動的bash進程中運行ip a看到的輸出:
root@vagrant:~$ ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
? ? link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ve1@if3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group ...
? ? link/ether 7a:8a:3f:ba:61:2c brd ff:ff:ff:ff:ff:ff link-netnsid 0
不行的是狀態(tài)DOWN表明這個接口現在無法使用悍引,為了從宿主機到容器來通,我們需要將這個叫ve1@if3的以太網接口激活缸剪,這意味著我們需要同時從宿主機上和容器內將這個兩端都激活吗铐。首先我們在宿主機上,也就是ve2端將虛擬以太網link激活杏节,運行命令:root@vagrant:~$ ip link set ve2 up唬渗,接著我們在容器中,也就是ve1端運行相同的命令后奋渔,再次通過ip a查看進程的可見網絡接口就會發(fā)現狀態(tài)為UP镊逝,在筆者的機器上輸出如下:
root@vagrant:~$ ip link set ve1 up
root@vagrant:~$ ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
? ? link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ve1@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP ...
? ? link/ether 7a:8a:3f:ba:61:2c brd ff:ff:ff:ff:ff:ff link-netnsid 0
? ? inet6 fe80::788a:3fff:feba:612c/64 scope link
? ? valid_lft forever preferred_lft forever
眼尖的同學會發(fā)現ve1@if3這個接口上根本沒有ip地址啊,為了容器進程和宿主機IP數據包相互可達嫉鲸,我們需要在以太網的兩端設置響應的IP地址撑蒜,在容器中運行命令:root@vagrant:~$ ip addr add 192.168.1.100/24 dev ve1來為容器端網絡端口設置IP段,同樣在宿主機上運行命令:root@vagrant:~$ ip addr add 192.168.1.200/24 dev ve1來為宿主機端的以太網設備配置IP地址段。
當我們分別在兩端設置好IP地址后座菠,容器的路由表也會插入相應的路由信息狸眼,如下的是筆者在bash進程中運行ip route看到的輸出:
root@myhost:~$ ip route
192.168.1.0/24 dev ve1 proto kernel scope link src 192.168.1.200
讀者可以實際操作一下,這表路由配置信息就允許我們將IP數據包發(fā)送到宿主機浴滴。
由于我們在前邊的文章中詳細介紹過cgroup的原理拓萌,因此咱這里就不在累述了,簡單來說升略,cgroup讓進程只能看到自己專屬的cgroup資源控制文件夾微王。cgroup命名空間和其他的命名空間比起來,被加入到Linux的時間靠后品嚣,大部分命名空間是在linux內核版本3.8被引入炕倘,而cgroup命名空間知道內核版本v4.6才被引入。
在Linux操作系統(tǒng)中翰撑,進程間可以通過共享內存或者消息隊列來進行通信罩旋,前提是兩個進程必須所屬相同的IPC命名空間。通常來說眶诈,我們不希望一個運行在一個進程中的應用程序訪問另外一個進程的內存瘸恼,因此大部分情況下進程都有自己獨享的IPC命名空間。
我們可以通過ipcmk和ipcs來驗證册养,ipcmk用來創(chuàng)建一個共享內存段,而ipcs用來返回IPC的狀態(tài)压固,在筆者的機器上輸出如下:
$ ipcmk -M 1000
Shared memory id: 98307
$ ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
48 | Chapter 4: Container Isolation
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0xad291bee 98307 ubuntu 644 1000 0
------ Semaphore Arrays --------
key semid owner perms nsems
0x000000a7 0 root 600 1
我們可以在啟動進程的時候球拦,通過unshare --ipc來給進程創(chuàng)建新的IPC命名空間,然后在進程中運行ipcs后帐我,從返回的結果就可以看到這是一個新的IPC命名空間坎炼,沒有任何進程間通信的結構。
最后我們來說說User命名空間拦键。User命名空間本質上讓進程有自己專屬的用戶(user)和組(group)視圖谣光。user命名空間和PID命名空間類似,只是在新的user命名空間中芬为,宿主機上的user和組信息可以有不同的ID萄金。
從安全的角度看,user命名空間是革命性的媚朦, 因為我們可以把容器中的0號用戶(root用戶)映射到宿主機上的非root用戶上氧敢,這樣即便是惡意攻擊者從容器中逃逸,那么在宿主機上也只持有一個權限有限的賬戶询张。
但是不幸的是孙乖,user命名空間目前還不是太被廣泛的使用,Docker上默認為開啟user命名空間,并且Kubernetes根本就不支持唯袄,不過這個命名空間在kubernetes社區(qū)正在積極的被討論是否加入竭恬。
我們來實踐一下扫夜,在自己機器上通過命令unshare --user bash來啟動一個新的bash進程,在進程中運行id命令返回用戶信息,如下輸出所示:
vagrant@vagrant:~$ unshare --user bash
nobody@vagrant:~$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
nobody@vagrant:~$ echo $$
31196
本質上來說晋被,在新的用戶命名空間中,宿主機上的用戶1000圆存,在宿主機上的ID是nobody玉掸,并且ID是65534,具體來說阎抒,用戶信息從宿主機映射到進程是通過文件/proc/<pid>/uid_map來完成酪我。感興趣的同學可以研究一下如何在root用戶權限下編輯這個文件。
比如我們?yōu)檫M程31196運行命令sudo echo '0 1000 1' > /proc/31196/uid_map后且叁,用戶1000在進程中就有了root權限都哭,我們可以通過運行id命令來驗證:
nobody@vagrant:~$ id
uid=0(root) gid=65534(nogroup) groups=65534(nogroup)
root用戶在容器進程內部有幾乎所有的權限,我們可以通過命令capsh --print | grep Current來驗證逞带,輸出的很長一串capabilities欺矫。咱們在前邊的文章介紹過Linux操作系統(tǒng)的capabilities機制,在容器內部展氓,內容允許這個“偽”root用戶可以干幾乎所有的事情穆趴,比如創(chuàng)建命名空間,設置網絡等遇汞。
如果我們在啟動進程的時候需要創(chuàng)建多個新的命名空間未妹,那么一般情況下user命名空間是首先會被創(chuàng)建出來,然后用戶會具備root權限空入,比如我們不通過root權限運行unshare --uts bash會出錯络它,但是運行unshare --uts --user bash不會報錯并成功。
注:Docker默認情況下并沒有turn on 用戶命名空間歪赢,主要原因是兼容性問題化戳。比如我們的應用程序在容器中以root權限運行,應用無法監(jiān)聽小于1024的端口埋凯,主要原因是容器的root權限沒有CAP_NET_BIND_SERVICE能力点楼。
從宿主機的角度來看,雖然我們通常把docker運行起來的應用程序稱作“容器實例”白对,但是從宿主機上看到的更多是“容器化的進程”盟步。運行應用程序的容器實例仍然是Linux操作系統(tǒng)上的一個進程,唯一的區(qū)別是這些叫容器的進程只能看到宿主機的一部分躏结,只能訪問操作系統(tǒng)文件系統(tǒng)的一部分却盘,只能使用機器的部分資源。
筆者想要強調的是,無論怎么看黄橘,運行在Docker中的容器實例是操作系統(tǒng)的一個進程兆览,雖說有自己的進程上下文,但是同一臺機器上的進程共享底層的操作系統(tǒng)內核塞关。我們可以同時從宿主機上和容器內部看到應用程序進程抬探,唯一的不同就是進程ID。宿主機上可以看到所有容器進程這個事實決定了容器的隔離邊界帆赢,從安全的角度來看小压,如果惡意攻擊者攻破了宿主機,那么黑客就有能力影響所有運行在這臺機器上的應用程序椰于。
通過這篇文章怠益,讀者應該很清楚一個事實,容器和宿主機共享同一個操作系統(tǒng)內核瘾婿,從安全的角度衍生了新的安全問題蜻牢。筆者強烈建議將容器進程運行在專屬的物理機或者虛擬機上(也就是這些機器上不要跑其他業(yè)務負載),原因主要是安全的考慮:
- 當我們使用像kubernetes這樣的容器編排平臺來部署應用程序偏陪,意味著運維人員基本不需要直接登陸到宿主機上抢呆。這樣安全性也就更好,因為宿主機上只需要跑類似于kubelets和kube proxy這樣的插件笛谦,攻擊面被極大的縮減抱虐。
- 我們在設計應用的部署架構時,可以考慮Thin OS饥脑,這種定制的OS只包含運行容器應用的必要組件梯码,降低了存在安全漏洞的風險。
- 集群中的宿主機做到immutable好啰,當一臺機器故障的時候,我們不是修復這臺機器儿奶,而是替換這臺機器框往,這樣所有機器的安全補丁受統(tǒng)一控制,管理更加容易闯捎。
好了椰弊,咱們這篇文章就這么多了,下篇文章我們繼續(xù)討論容器和虛擬機的差異瓤鼻,這是我們理解容器化部署的基石秉版,敬請期待!