同一個POD中預設共享哪些名稱空間
如果通過POD的形式來啟動多個容器那麼它們的名稱空間會是共享的麼,所以我這裡討論是在預設情況下同一個POD的不同容器的哪些名稱空間是打通的。這裡先說一下結論,共享的是UTS、IPC、NET、USER。
UTS名稱空間
主機名名稱空間,儲存核心名稱、版本以及主機名和域名。預設情況下同一個POD的不同容器是共享UTS的,看下面的配置:
apiVersion: apps/v1 kind: Deployment metadata: name: centos-dep labels: app: centos spec: replicas: 1 selector: matchLabels: app: centos template: metadata: labels: app: centos spec: containers: - name: app1 image: centos imagePullPolicy: IfNotPresent command: ["/bin/sh", "-c"] args: - sleep 3600 - name: app2 image: centos imagePullPolicy: IfNotPresent command: ["/bin/sh", "-c"] args: - sleep 3600
執行這個POD,然後分別登陸到不同容器去檢視主機名,你會發現主機名一樣,而且就是POD的名字,如下圖:
另外你通過uname -a
如果檢視到的內容是一致的也說明是共享UTS名稱空間的。
實驗證明,預設情況下同一個POD中的不同容器的UTS名稱空間是共享的。
IPC名稱空間
程序間通訊名稱空間,IPC的隔離就是阻斷程序間通訊,主要是訊號量、佇列和共享記憶體。執行主機程序通過上面的機制進行通訊。
下面通過一個實驗來看看同一個POD的IPC名稱空間是否是共享的,在app2中通過命令ipcmk --queue
來建立一個佇列,然後在app1中通過命令ipcs
來檢視,如果有這個佇列就說明是共享的,如下圖:
實驗證明,預設情況下同一個POD中的不同容器的IPC名稱空間是共享的。
MNT名稱空間
Mount名稱空間,提供對磁碟掛載點和檔案系統的隔離能力。同一主機上的不同程序訪問相同的路徑會得到相同的內容,因為它們共享本地主機的磁碟和檔案系統。
在同一POD內容器之間掛載點名稱空間是隔離的,如果該POD的多個容器掛載一個POD級別的Volume,那麼它們就可以實現掛載點的共享,但共享的也僅僅是這一個Volume並不是整個檔案系統。
實驗證明,預設情況下同一個POD中的不同容器的MNT名稱空間不是共享的。
NET名稱空間
網路名稱空間,同一主機上的不同程序可以進行localhost或者本地unix socket通訊。在單獨啟動容器的時候不同容器是隔離的,但是在POD中不同容器通過一個Infra容器來進行共享網路名稱空間,其原理是其他使用者自己定義的容器都Join這個Infra容器的網路。這裡我啟動的就是一個Cetnos映象,無法做本地通訊驗證。不過它的確是通過Infra容器來共享的。
PID名稱空間
程序ID名稱空間,同一主機上的不同程序在同一PID空間內可以看到其他程序的ID,並且同一PID空間的程序的ID不會重複。另外PID名稱空間有層級關係,子空間看不到父空間的內容,但是父空間可以管理子空間,比如傳送訊號。
在POD中則對應為同一POD內的不同容器可以看到對方的程序ID。預設不是共享的,可以設定POD的shareProcessNamespace
這個值為true來進行共享,預設為false。我在App2中啟動一個top命令,然後在App1中通過ps命令檢視,看下面的測試:
實驗證明,預設情況下同一個POD中的不同容器的PID名稱空間不是共享的。
USER名稱空間
隔離使用者、組以及相關使用者能力的。也就是在不同的User Namespace中,相同的使用者可以有不同的UID或者不同的許可權。另外還可以通過對映的方式把某個User Namespace的使用者對映到另外一個User Namespace的使用者上,這樣這兩個名稱可能不同的使用者就具有相同的許可權。如果想要在本機進行驗證需要檢視一下這個檔案:
cat /proc/sys/user/max_user_namespaces
如果是0則表示沒有開啟,需要給它一個值echo "15000" > /proc/sys/user/max_user_namespaces
,然後你再執行unshare -U或者unshare --user
就不會報錯了。
在Docker中預設並沒有開啟user namespace。
可以看到當前Bash程序和Dockerd程序的名稱空間都一樣,因為它們都是在同一個名稱空間上執行的。另外需要說明的是uip_map的輸出,第一個數字是在當前名稱裡的使用者ID,第二個數字是該使用者ID在當前名稱空間外部被對映到哪個使用者ID上,最後一個數字是對映範圍。
然後我們啟動一個包含兩個容器的POD來看一下,如下圖:
容器的User namespace和容器外的是一樣的,也就是說沒有單獨為容器建立User namespace,而且容器內的使用者ID是0,對映到容器外也是0,這就是意味著容器內的root使用者和容器外的root使用者擁有相同的許可權。說白了就是容器中的程序是以root使用者許可權執行的,並且這個容器中的root使用者和宿主機上的root使用者是同一個,看下圖,這2個容器程序就是以root執行的:
如果你需要驗證,那麼你把宿主機上的一個只能由root開啟的檔案掛載容器中,你看看能不能開啟就知道了。
就算你進入容器檢視這個sleep 3600其實也是root執行的,簡單來說容器內UID為0的root使用者就是容器外UID為0的root使用者。為什麼會是這樣呢?在整個系統共享一個核心,而核心只管理一套uid和gid,並且對核心來說只識別uid不識別使用者名稱,也就是說核心在做許可權方面它通過uid來做,使用者名稱只是對於使用者來講方便辨認。
不要誤認為你在容器中建立一個使用者,然後在宿主機也可以看到,因為/etc/password這個檔案在不同的檔案系統上,容器和宿主機的檔案系統還是隔離的。
但有些時候也不要被使用者名稱所迷惑,你應該檢查UID,檢視容器程序的uid_map中的資訊。
讓容器程序使用root賬號顯然不安全,因為它的root就是宿主機的root,所以通常我們會給dockerd程序建立單獨的賬號或者使用User Namespace。不過推薦使用User Namesapce,因為有些使用容器程序必須以root來執行,如果使用User Namespace的話,我們就可以把宿主機的一個普通使用者對映到容器中的root使用者,這樣容器程序以為自己是root並且在它所在的名稱空間內有各種許可權,但是在宿主機上它還是普通使用者。
如何開啟User Namespace呢:
cat /boot/config-3.10.0-957.el7.x86_64 | grep _NS
,先檢查一下你的核心是否開啟了User Namespace
檢查一下是否有下面的檔案,如果沒有就手動建立:
你可以使用系統中有的使用者然後新增到這裡,最後在docker的啟動引數中加入這個賬號,也可以讓dockerd自己來建立,如果讓dockerd自己來完成,在dockerd的啟動docker-daemon.json中加入下面的內容,default表示使用dockerd去建立賬號,它使用的名字為dockermap,如果你使用自己的就替換dufault:
{
"userns-remap": "default",
}
在RHEL 7.5版本,上面的配置在dockerd啟動的時候會報錯"Can't create ID mappings: %!v(MISSING): No subuid ranges found for user "dockremap"",查詢之後判斷應該是系統BUG,可以看看Redhat官網的Bug說明Bug-1546870,它會在系統中建立dockremap賬號然後使用usermod -v引數來設定dockermap使用者的ID範圍,但是在Centos 7.5版本上的usermod命裡沒有-v引數。這就意味著RHEL 7.5不支援動態新增subid。所以我們只能手動來做,不過據說其他發行版可以支援比如Ubantu或者Fedora。
向從屬使用者和組檔案中新增範圍(如果你使用dockremap賬號,那麼你無須手動建立,因為dockerd啟動的時候就會建立,如果上面的配置是default):
echo "dockremap:10000:65536" > /etc/subuid
echo "dockremap:10000:65536" > /etc/subgid
一共三個欄位:
第一個欄位dockremap,這個一個宿主機上的使用者名稱
第二個欄位10000,表示子User Namespace中使用者ID從哪裡開始
第三個欄位65536,表示子User Namespace中可以有多少個使用者ID
整體含義是宿主機的dockremap賬號一共有65536個從屬使用者,使用者ID從10000-165535。這個從事使用者的ID不是真實的,只是用來分配,它會從這個範圍裡拿一個ID對映到容器程序裡的使用者,比如容器程序還是用root使用者,其UID實0,那麼我們就可以從dockremap這個從屬ID中拿一個來對映容器程序中的root。這樣容器中看起來是root且具有root許可權,但是在宿主機上它就是一個普通賬號dockremap的許可權。配置好後重啟dockerd程序。配置好重新啟動POD,如下:
同一個POD中的User Namespace是共享的,但此時它與宿主機的程序就已經不共享User Namespace了。再看一下uid_map
容器中的UID0對映到容器外的從屬ID 10000。
不過這樣雖然安全但是有些容器程序無論在容器內還是在容器外都需要root賬號,比如prometheus的node_explorer,它是以DaemonSet形式執行的需要共享宿主機的網路名稱空間,如果以上的使用者來執行則會啟動失敗,如下圖:
其實這個和DaemonSet沒關係,主要是在docker上啟用User Namspace後會有一些限制,userns-remap ,也就是說啟用了User Namespace後容器將不能共享宿主機的PID和NET名稱空間。所以我想因為有一些限制所以docker預設才不開啟User Namespace。不過如果直接通過docker來啟動容器可以指定--usens=host
來為某個容器禁用User Namespace,不過在Kubernetes中目前沒找到配置POD那個引數可以起到這個效果,有人知道請留言。
實驗證明:在預設情況下同一個POD是共享User Namespace的。
最簡單的辦法來驗證一下
在宿主機上找到該POD中的2個容器的容器ID,
通過docker inspect CONTINER_ID --format {{.State.Pid}}
檢視兩個容器在宿主機上的程序號
通過程序ID檢視每個程序的ns情況,左側紅色的是被檢視程序名稱空間檔案,右側則是該檔案指向的具體的Namespace檔案,中括號裡面的是具體Namespace檔案號,如果兩個程序的指向的Namespace檔案號相同,則說明它們處在同一名稱空間。
紅色箭頭編號相同的就是當前POD中2個容器所共享的名稱空間。不過在這裡我也有些不明白,uts是共享的可是上圖中看到的編號確不一樣。因為在宿主機的當前終端執行unshare --uts /bin/bash
命令將會在一個新的uts名稱空間開啟一個bash程式,這個bash程序和之前那個就是在不同的uts中,看下圖:
進行namespace的api操作
對Namespace的API操作包括clone()、setns()和unshare()。它們有一些不同:
clone():建立新進場的同時可以建立namespace,通過在這個函式中加入不同的名稱空間標誌來完成。
setns():它是加入一個已經存在的namespace,需要給它傳遞具體的namespace檔案描述符。通常是在呼叫該函式之後呼叫clone(),其目的就是讓一個新程序在一個已經存在的namespace中執行。
docker exec
就是利用這種機制讓你指定的命令在容器中執行。unshare():對當前的程序進行namespace隔離,換句話說它不啟動新程序,而是讓當前程序或者呼叫它的程序進入到一個新的namespace中。系統命令unshare就是利用這個呼叫來實現的。
注意在使用unshare系統呼叫或者命令或者setns系統呼叫的時候當涉及到PID Namespace的時候它的處理有些特殊,並不是讓呼叫者進入新的PID Namespace,而是讓子程序進入,成為該PID Namespace的1號程序。為什麼為這樣呢?因為一個程序的PID在系統中是常量,一但一個程序執行它的PID就確定了從而它的父子程序也會被確定,所以不能讓它在呼叫setns或者unshare的時候發生變化,一但變化系統就無法維護這個程序表。
Namespace的資源隔離
Docker背後的核心知識1
Linux Namespace User
理解Docker容器的UID和GID
隔離Docker容器中的用