同一個POD中預設共享哪些名稱空間

昀溪發表於2019-06-13

如果透過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容器中的使用者

相關文章