User namespace 是 Linux 3.8 新增的一種 namespace,用於隔離安全相關的資源,包括 user IDs and group IDs,keys, 和 capabilities。同樣一個使用者的 user ID 和group ID 在不同的 user namespace 中可以不一樣(與 PID nanespace 類似)。換句話說,一個使用者可以在一個user namespace中是普通使用者,但在另一個 user namespace 中是超級使用者。
User namespace 可以嵌套(目前核心控制最多32層),除了系統預設的 user namespace 外,所有的 user namespace 都有一個父 user namespace,每個 user namespace 都可以有零到多個子 user namespace。 當在一個進程中調用 unshare 或者 clone 建立新的 user namespace 時,當前進程原來所在的 user namespace 為父 user namespace,新的 user namespace 為子 user namespace。
說明:本文的示範環境為 ubuntu 16.04。
建立 user namespace
我們可以通過 unshare 命令的 --user 選項來建立新的 user namespace:
$ unshare -user -r /bin/bash
通過 -r 參數,我們把新的 user namespace 中的 root 使用者映射到了外面的 nick 使用者(接下來會介紹映射相關的概念)。在新的 user namespace 中,root 使用者是有許可權建立其它的 namespace 的,比如 uts namespace。這是因為當前的 bash 進程擁有全部的 capabilities:
下面我們建立一個新的 uts namespace 試試:
$ unshare --uts /bin/bash
我們看到,新的 uts namespace 被順利的建立了。這是因為除了 user namespace 外,建立其它類型的 namespace 都需要 CAP_SYS_ADMIN 的 capability。當新的 user namespace 建立並映射好 uid、gid 了之後, 這個 user namespace 的第一個進程將擁有完整的所有 capabilities,意味著它就可以建立新的其它類型 namespace。
其實沒有必要把上面的操作(建立兩個 namespace)分成兩步,我們可以通 unshare 一次建立多個 namespace:
在 unshare 的實現中,其實就是傳入了 CLONE_NEWUSER | CLONE_NEWUTS,大致如下:
unshare(CLONE_NEWUSER | CLONE_NEWUTS);
在上面這種情況下,核心會保證 CLONE_NEWUSER 先被執行,然後執行剩下的其他 CLONE_NEW*,這樣就使得不用 root 使用者而建立新的容器成為可能,這條規則對於clone 函數也同樣適用。
理解 UID 和 GID 的映射
在前面的示範中我們提到了使用者在 user namespace 之間的映射,下面我們同樣通過示範來理解映射是什麼。我們先查看下目前使用者的 ID 和 user namespace 情況:
然後執行 unshare --user /bin/bash 命令建立一個新的 user namespace,注意這次沒 -r 參數:
$ unshare --user /bin/bash
在新的 user namespace 中,目前使用者變成了 nobody,並且 ID 也變成了 65534。
這是因為我們還沒有映射父 user namespace 的 user ID 和 group ID 到子 user namespace 中來,這一步是必須的,因為這樣系統才能控制一個 user namespace 裡的使用者在其他 user namespace 中的許可權(比如給其它 user namespace 中的進程發送訊號,或者訪問屬於其它 user namespace 掛載的檔案)。
如果沒有映射,當在新的 user namespace 中用 getuid() 和 getgid() 擷取 user ID 和 group ID 時,系統將返迴文件 /proc/sys/kernel/overflowuid 中定義的 user ID 以及 proc/sys/kernel/overflowgid 中定義的 group ID,它們的預設值都是 65534。也就是說如果沒有指定映射關係的話,會預設會把 ID 映射到 65534。
下面我們來完成 nick 使用者在新的 user namespace 中的映射。
映射 ID 的方法就是添加映射資訊到 /proc/PID/uid_map 和 /proc/PID/gid_map (這裡的 PID 是新 user namespace 中的進程 ID,剛開始時這兩個檔案都是空的)檔案中。這兩個檔案中的配置資訊的格式如下(每個檔案中可以有多條配置資訊):
ID-inside-ns ID-outside-ns length
比如 0 1000 500 這條配置就表示父 user namespace 中的 1000~1500 映射到新 user namespace 中的 0~500。
對 uid_map 和 gid_map 檔案的寫入操作有著嚴格的許可權控制,簡單點說就是:這兩個檔案的擁有者是建立新的 user namespace 的使用者,所以和這個使用者在一個 user namespace 中的 root 帳號可以寫;這個使用者自己是否有寫 map 檔案的許可權還要看它有沒有 CAP_SETUID 和 CAP_SETGID 的 capability。注意:只能向 map 檔案寫一次資料,但可以一次寫多條,並且最多隻能 5 條。
我們把剛才開啟的 shell 視窗稱為第一個 shell 視窗開始執行使用者的映射操作(把使用者 nick 映射為新 user namespace 中的 root)。
第一步,先在第一個 shell 視窗中查看當前進程的 ID:
第二步,新開啟一個 shell 視窗,我稱之為第二個 shell 視窗。查看進程 3049 的對應檔屬性:
使用者 nick 是這兩個檔案的所有者,讓我們嘗試向這兩個檔案寫入映射資訊:
看上去很奇怪呀,明明是檔案的所有者,卻沒有許可權向檔案中寫入內容!其實根本的原因在於當前的 bash 進程沒 CAP_SETUID 和 CAP_SETGID 的許可權:
下面我們為 /bin/bash 程式設定相關的 capabilities:
$ sudo setcap cap_setgid,cap_setuid+ep /bin/bash
然後重新載入 bash,就可以看到相應的 capabilities 了:
現在重新向 map 檔案寫入映射資訊:
$ echo '0 1000 500' > /proc/3049/uid_map$ echo '0 1000 500' > /proc/3049/gid_map
這次的寫入成功了。後面就不需要我們手動寫入映射資訊了,所以我們通過下面的命令把 /bin/bash 的 capability 恢複為原來的設定:
$ sudo setcap cap_setgid,cap_setuid-ep /bin/bash
第三步,回到第一個 shell 視窗
重新載入 bash,並執行 id 命令:
目前使用者已經變成了 root(新的 user namespace 中的 root 使用者)。在看看當前 bash 進程具有的 capability:
0000003fffffffff 表示當前啟動並執行 bash 擁有所有的 capability。
第四步,在第一個 shell 視窗中
查看 /root 目錄的存取權限:
沒許可權啊!嘗試修改主機的名稱:
依然是沒有許可權啊!看來這個新 user namespace 中的 root 使用者在父 user namespace 裡面不好使。這也正是 user namespace 所期望達到的效果,當訪問其它 user namespace 裡的資源時,是以其它 user namespace 中的相應使用者的許可權來執行的,比如這裡 root 對應父 user namespace 的使用者是 nick,所以改不了系統的 hostname。
普通使用者 nick 沒有修改 hostname 的許可權,那把預設的 user namespace 中的 root 使用者映射為子 user namespace 中的 root 使用者後可以修改 hostname 嗎?答案是,不行!那是因為不管怎麼映射,當用子 user namespace 的使用者訪問父 user namespace 的資源的時候,它啟動的進程的 capability 都為空白,所以這裡子 user namespace 的 root 使用者在父 user namespace 中就相當於一個普通的使用者。
User namespace 與其它 namespace 的關係
Linux 下的每個 namespace,都有一個 user namespace 與之關聯,這個 user namespace 就是建立相應 namespace 時進程所屬的 user namespace,相當於每個 namespace 都有一個 owner(user namespace),這樣保證對任何 namespace 的操作都受到 user namespace 許可權的控制。這也是為什麼在子 user namespace 中設定 hostname 失敗的原因,因為要修改的 uts namespace 屬於的父 user namespace,而新 user namespace 的進程沒有老 user namespace 的任何 capabilities。
以 uts namespace 為例,在 uts_namespace 的結構體中有一個指向 user namespace 的指標,指向它所屬的 user namespace(筆者查看的 v4.13核心,uts_namespace 結構體的定義在 /include/linux/utsname.h 檔案中):
其它 namespace 的定義也是類似的。
總結
相對其它的 namespace 而言,user namespace 稍顯複雜。這是由其功能決定的,涉及到許可權管理的內容時,事情往往會變得不那麼直觀。筆者在本文中也只是介紹了 user namespace 的基本概念,更多豐富有趣的內容還有待大家自行發掘。
參考:
user namespace man page
Linux Namespace系列(07):user namespace (CLONE_NEWUSER) (第一部分)
Linux Namespace系列(08):user namespace (CLONE_NEWUSER) (第二部分)
Namespaces in operation, part 5: User namespaces
Namespaces in operation, part 6: more on user namespaces
Linux capabilities