理解 Docker 容器中 UID 和 GID 的工作原理
理解用戶名、組名、用戶ID(UID)和組ID(GID)在容器內運行的進程與主機系統之間的映射是構建安全系統的重要一環。如果沒有提供其他選項,容器中的進程將以root用戶身份執行(除非在Dockerfile中提供了不同的UID)。本文將解釋這一工作原理,如何正確授予權限,并提供示例加以說明。
逐步分析uid/gid安全性
首先,讓我們回顧一下uid和gid是如何實現的。Linux內核負責管理uid和gid空間,使用內核級系統調用來確定是否應該授予請求的特權。例如,當一個進程嘗試寫入文件時,內核會檢查創建該進程的uid和gid,以確定它是否具有足夠的特權來修改文件。這里不使用用戶名,而是使用uid。
在服務器上運行 Docker 容器時,仍然只有一個內核。容器化帶來的巨大價值之一是所有這些獨立的進程可以繼續共享一個內核。這意味著即使在運行 Docker 容器的服務器上,整個 uid 和 gid 的世界仍由一個單一內核控制。
因此,在不同的容器中不能使用相同的 uid 分配給不同的用戶。這是因為在常見的 Linux 工具中顯示的用戶名(和組名)并不是內核的一部分,而是由外部工具(如 /etc/passwd、LDAP、Kerberos 等)管理。因此,你可能會看到不同的用戶名,但是即使在不同的容器中,對于相同的 uid/gid,你也不能擁有不同的權限。這一點一開始可能會讓人感到相當困惑,所以讓我們通過幾個例子來說明一下:
簡單的Docker運行
我將首先以普通用戶(marc)的身份登錄到一個屬于docker組的服務器上。這樣我就可以在不使用sudo命令的情況下啟動docker容器。然后,從容器外部,讓我們來看看這個過程是如何呈現的。
marc@server:~$ docker run -d ubuntu:latest sleep infinity
92c57a8a4eda60678f049b906f99053cbe3bf68a7261f118e411dee173484d10
marc@server:~$ ps aux | grep sleep
root 15638 0.1 0.0 4380 808 ? Ss 19:49 0:00 sleep infinity
盡管我從未輸入過sudo,也不是root用戶,但我執行的sleep命令以root用戶身份啟動并具有root權限。我如何知道它具有root權限?容器內的root是否等同于容器外的root?是的,因為正如我提到的,有一個單一的內核和一個共享的uid和gid池。由于容器外顯示的用戶名是“root”,我可以確定容器內的進程是以具有uid = 0的用戶啟動的。
帶有定義用戶的Dockerfile
當我在 Dockerfile 中創建一個不同的用戶并以該用戶身份啟動命令時會發生什么?為了簡化這個例子,我這里沒有指定 gid,但相同的概念也適用于組 id。
首先,我正在以用戶名為“marc”的用戶身份運行這些命令,該用戶的用戶ID為1001。
marc@server:~$ echo $UID
1001
Dockerfile文件:
FROM ubuntu:latest
RUN useradd -r -u 1001 -g appuser appuser
USER appuser
ENTRYPOINT [“sleep”, “infinity”]
構建:
marc@server:~$ docker build -t test .
Sending build context to Docker daemon 14.34 kB
Step 1/4 : FROM ubuntu:latest
— -> f49eec89601e
Step 2/4 : RUN useradd -r -u 1001 appuser
— -> Running in 8c4c0a442ace
— -> 6a81547f335e
Removing intermediate container 8c4c0a442ace
Step 3/4 : USER appuser
— -> Running in acd9e30b4aba
— -> fc1b765e227f
Removing intermediate container acd9e30b4aba
Step 4/4 : ENTRYPOINT sleep infinity
— -> Running in a5710a32a8ed
— -> fd1e2ab0fb75
Removing intermediate container a5710a32a8ed
Successfully built fd1e2ab0fb75
marc@server:~$ docker run -d test
8ad0cd43592e6c4314775392fb3149015adc25deb22e5e5ea07203ff53038073
marc@server:~$ ps aux | grep sleep
marc 16507 0.3 0.0 4380 668 ? Ss 20:02 0:00 sleep infinity
marc@server:~$ docker exec -it 8ad0 /bin/bash
appuser@8ad0cd43592e:/$ ps aux | grep sleep
appuser 1 0.0 0.0 4380 668 ? Ss 20:02 0:00 sleep infinity
這里到底發生了什么,這意味著什么?我構建了一個 Docker 鏡像,其中有一個名為“appuser”的用戶,該用戶的 uid 為 1001。在我的測試服務器上,我使用的帳戶名為“marc”,uid 也是 1001。當我啟動容器時,sleep 命令以 appuser 的身份執行,因為 Dockerfile 包含了“USER appuser”這一行。但實際上這并不是以 appuser 的身份運行,而是以 Docker 鏡像中被識別為 appuser 的用戶的 uid 運行。
當我檢查容器外運行的進程時,我發現它映射到用戶“marc”,但在容器內部,它映射到用戶“appuser”。這兩個用戶名只是顯示它們的執行上下文所知道的映射到1001的用戶名。
這并不是非常重要。但重要的是要知道,在容器內部,用戶“appuser”獲得了來自容器外部用戶“marc”的權限和特權。在Linux主機上授予用戶marc或uid 1001的權限也將授予容器內的appuser這些權限。
如何控制容器的訪問權限
另一種選擇是在運行 Docker 容器時指定用戶名或用戶ID,以及組名或組ID。
再次使用上面的初始示例。
marc@server:~$ docker run -d --user 1001 ubuntu:latest sleep infinity
84f436065c90ac5f59a2256e8a27237cf8d7849d18e39e5370c36f9554254e2b
marc@server$ ps aux | grep sleep
marc 17058 0.1 0.0 4380 664 ? Ss 21:23 0:00 sleep infinity
我在這里做了什么?我創建了容器以1001用戶身份啟動。因此,當我執行諸如ps或top(或大多數監控工具)之類的命令時,進程映射到“marc”用戶。
有趣的是,當我進入該容器時,你會發現1001用戶在/etc/passwd文件中沒有條目,并在容器的bash提示符中顯示為“I have no name!”。
marc@server:~$ docker exec -it 84f43 /bin/bash
I have no name!@84f436065c90:/$
重要的是要注意,在創建容器時指定用戶標志也會覆蓋 Dockerfile 中的值。還記得第二個例子嗎?那時我使用了一個 Dockerfile,其中的 uid 映射到本地主機上的不同用戶名。當我們在命令行上使用用戶標志來啟動一個執行“sleep infinity”進程的容器時,會發生什么呢?
marc@server:$ docker run -d test
489a236261a0620e287e434ed1b15503c844648544694e538264e69d534d0d65
marc@server:~$ ps aux | grep sleep
marc 17689 0.2 0.0 4380 680 ? Ss 21:28 0:00 sleep infinity
marc@server:~$ docker run --user 0 -d test
ac27849fcbce066bad37190b5bf6a46cf526f56d889af61e7a02c3726438fa7a
marc@server:~$ ps aux | grep sleep
marc 17689 0.0 0.0 4380 680 ? Ss 21:28 0:00 sleep infinity
root 17783 0.3 0.0 4380 668 ? Ss 21:28 0:00 sleep infinity
在上面的最后一個示例中,您可以看到我最終得到了兩個運行睡眠進程的容器,一個是“marc”,另一個是“root”。這是因為第二個命令通過在命令行上傳遞--user標志來更改了用戶ID。
總結
現在我們已經探討了這一點,可以理解以有限權限運行容器的方式都利用了主機的用戶系統:
- 如果容器內部的進程正在執行的已知 uid,那么簡單地限制對主機系統的訪問,使容器中的 uid 僅具有有限訪問權限就可以了。
- 更好的解決方案是使用--user以已知 uid 啟動容器(也可以使用用戶名,但請記住這只是提供主機用戶名系統中的 uid 的一種更友好的方式),然后限制主機上您決定容器將以其運行的 uid 的訪問權限。
- 由于容器到主機的 uid 和用戶名(以及 gid 和組名)的映射,指定容器化進程運行的用戶可以使該進程在容器內部和外部看起來像是由不同的用戶擁有。