理解用户名、组名、用户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 和组名)的映射,指定容器化进程运行的用户可以使该进程在容器内部和外部看起来像是由不同的用户拥有。