理解 docker 容器中的 uid 和 gid
預設情況下,容器中的程序以 root 使用者許可權執行,並且這個 root 使用者和宿主機中的 root 是同一個使用者。聽起來是不是很可怕,因為這就意味著一旦容器中的程序有了適當的機會,它就可以控制宿主機上的一切!本文我們將嘗試瞭解使用者名稱、組名、使用者 id(uid)和組 id(gid)如何在容器內的程序和主機系統之間對映,這對於系統的安全來說是非常重要的。說明:本文的演示環境為 ubuntu 16.04(下圖來自網際網路)。
先來了解下 uid 和 gid
uid 和 gid 由 Linux 核心負責管理,並通過核心級別的系統呼叫來決定是否應該為某個請求授予特權。比如當程序試圖寫入檔案時,核心會檢查建立程序的 uid 和 gid,以確定它是否有足夠的許可權修改檔案。注意, 核心使用的是 uid 和 gid,而不是使用者名稱和組名 。
簡單起見,本文中剩下的部分只拿 uid 進行舉例,系統對待 gid 的方式和 uid 基本相同。
很多同學簡單地把 docker 容器理解為輕量的虛擬機器,雖然這簡化了理解容器技術的難度但是也容易帶來很多的誤解。事實上,與虛擬機器技術不同:同一主機上執行的所有容器共享同一個核心(主機的核心)。容器化帶來的巨大價值在於所有這些獨立的容器(其實是程序)可以共享一個核心。這意味著即使由成百上千的容器執行在 docker 宿主機上,但 核心控制的 uid 和 gid 則仍然只有一套 。所以同一個 uid 在宿主機和容器中代表的是同一個使用者(即便在不同的地方顯示了不同的使用者名稱)。
注意,由於普通的用來顯示使用者名稱的 Linux 工具並不屬於核心(比如 id 等命令),所以我們可能會看到同一個 uid 在不同的容器中顯示為不同的使用者名稱。但是對於相同的 uid 不能有不同的特權,即使在不同的容器中也是如此。
如果你已經瞭解了 Linux 的 user namespace 技術,參考《 Linux Namespace : User 》,你需要注意的是到目前為止,docker 預設並沒有啟用 user namesapce,這也是本文討論的情況。筆者會在接下來的文章中介紹如何配置 docker 啟用 user namespace。
容器中預設使用 root 使用者
如果不做相關的設定,容器中的程序預設以 root 使用者許可權啟動,下面的 demo 使用 ubuntu 映象執行 sleep 程式:
$ docker run -d--name sleepme ubuntu sleep infinity
注意上面的命令中並沒有使用 sudo。筆者在宿主機中的登入使用者是 nick,uid 為 1000:
在宿主機中檢視 sleep 程序的資訊:
$ ps aux | grep sleep
sleep 程序的有效使用者名稱稱是 root,也就是說 sleep 程序具有 root 許可權。
然後進入容器內部看看,看到的情況和剛才一樣,sleep 程序也具有 root 許可權:
那麼, 容器內的 root 使用者和宿主機上的 root 使用者是同一個嗎?
答案是:是的,它們對應的是同一個 uid。原因我們在前面已經解釋過了:整個系統共享同一個核心,而核心只管理一套 uid 和 gid。
其實我們可以通過資料捲來簡單的驗證上面的結論。在宿主機上建立一個只有 root 使用者可以讀寫的檔案:
然後掛載到容器中:
$ docker run --rm -it -w=/testv -v $(pwd)/testv:/testv ubuntu
在容器中可以讀寫該檔案:
我們可以通過 Dockerfile 中的 USER 命令或者是 docker run 命令的 --user 引數指定容器中程序的使用者身份。下面我們分別來探究這兩種情況。
在 Dockerfile 中指定使用者身份
我們可以在 Dockerfile 中新增一個使用者 appuser,並使用 USER 命令指定以該使用者的身份執行程式,Dockerfile 的內容如下:
FROM ubuntu RUN useradd -r -u 1000 -g appuser USER appuser ENTRYPOINT ["sleep", "infinity"]
編譯成名稱為 test 的映象:
$ docker build -t test .
用 test 映象啟動一個容器:
$ docker run -d --name sleepme test
在宿主機中檢視 sleep 程序的資訊:
這次顯示的有效使用者是 nick,這是因為在宿主機中,uid 為 1000 的使用者的名稱為 nick。再進入到容器中看看:
$ docker exec -it sleepme bash
容器中的當前使用者就是我們設定的 appuser,如果檢視容器中的 /etc/passwd 檔案,你會發現 appuser 的 uid 就是 1000,這和宿主機中使用者 nick 的 uid 是一樣的。
讓我們再建立一個只有使用者 nick 可以讀寫的檔案:
同樣以資料卷的方式把它掛載到容器中:
$ docker run -d --name sleepme -w=/testv -v $(pwd)/testv:/testv test
在容器中 testfile 的所有者居然變成了 appuser,當然 appuser 也就有許可權讀寫該檔案。
這裡到底發生了什麼?而這些又這說明了什麼?
首先,宿主機系統中存在一個 uid 為 1000 的使用者 nick。其次容器中的程式是以 appuser 的身份執行的,這是由我們通過 USER appuser 命令在 Dockerfile 程式中指定的。
事實上,系統核心管理的 uid 1000 只有一個,在宿主機中它被認為是使用者 nick,而在容器中,它則被認為是使用者 appuser。
所以有一點我們需要清楚:在容器內部,使用者 appuser 能夠獲取容器外部使用者 nick 的權利和特權。在宿主機上授予使用者 nick 或 uid 1000 的特權也將授予容器內的 appuser。
從命令列引數中自定使用者身份
我們還可以通過 docker run 命令的 --user 引數指定容器中程序的使用者身份。比如執行下面的命令:
$ docker run -d --user 1000 --name sleepme ubuntu sleep infinity
因為我們在命令列上指令了引數 --user 1000,所以這裡 sleep 程序的有效使用者顯示為 nick。進入到容器內部看一下:
$ docker exec -it sleepme bash
這是個什麼情況?使用者名稱稱居然顯示為 "I have no name!"!去檢視 /etc/passwd 檔案,裡面果然沒有 uid 為 1000 的使用者。即便沒有使用者名稱稱,也絲毫不影響該使用者身份的許可權,它依然可以讀寫只有 nick 使用者才能讀寫的檔案,並且使用者資訊也由 uid 代替了使用者名稱:
需要注意的是,在建立容器時通過 docker run --user 指定的使用者身份會覆蓋掉 Dockerfile 中指定的值。
我們重新通過 test 映象來執行兩個容器:
$ docker run -d test
檢視 sleep 程序資訊:
$ docker run --user 0 -d test
再次檢視 sleep 程序資訊:
指定了 --urser 0 引數的程序顯示有效使用者為 root,說明命令列引數 --user 0 覆蓋掉了 Dockerfile 中 USER 命令的設定。
總結
從本文中的示例我們可以瞭解到,容器中執行的程序同樣具有訪問主機資源的許可權(docker 預設並沒有對使用者進行隔離),當然一般情況下容器技術會把容器中程序的可見資源封鎖在容器中。但是通過我們演示的對資料卷中檔案的操作可以看出,一旦容器中的程序有機會訪問到宿主機的資源,它的許可權和宿主機上使用者的許可權是一樣的。所以比較安全的做法是為容器中的程序指定一個具有合適許可權的使用者,而不要使用預設的 root 使用者。當然還有更好的方案,就是應用 Linux 的 user namespace 技術隔離使用者,筆者會在接下來的文章中介紹如何配置 docker 開啟 user namespace 的支援。
參考:
Understanding how uid and gid work in Docker containers