【翻译】理解容器中的uid和gid的工作方式

理解容器和宿主机之间的uid和gid是如何映射的,这对于容器安全运维非常重要。如果不加选项,容器中的进程是默认使用root用户运行,除非在Dockerfile中有指定为其他用户。这篇文章将会想你uid和gid在容器是如何工作的,并且会想你详细展示过程。

一步一步分析uid和gid

首先,我们来回顾下uid和gid在内核中是如何被实现的,内核通过uid和gui来判断当前系统调用是否有权限。例如:当一个试图向某个文件中写入文件的时候,内行会检查当程序的uid和gid,并且判断是否权限修改这个文件,检查权限的时候是不区分用户名的。

当一个容器运行在服务器上的时候,事实上这个容器中的进程和宿主机器共用一个系统内核。容器化带来的巨大好处是,在分割了进程的同时公用一个内核。这也就意味着,运行在容器中的进程,他们进程的uid和gid被同一个内核管理。

你不能在不同的容器中使用不同的用户名但是用着相同的uid。因为系统中显示用户名和组名的工具不属于系统内核,他们被外部的/etc/passwd,LDAP等工具管理者,所以你能看到不同的用户名和组名,但是他们不会有相同的uid和gid,在不同的容器中。接下来让我用一些小例子来解释。

简单的Docker run

接下我将使用marc这个用户名,这个用户名已经事先加入到了doker group中了,不需要使用sudo就能启动一个容器了,在容器之外我们能看到如下信息:

1
2
3
4
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用户,但是sleepe这个进程却拥有root权限。我是如何如何发现他有root权限的能?容器内的root和容器外的root是等价的吗?是的,因为他们是允许在同一系统内核上的,sleep在容器外展示的用户名是root,所以能断定容器的uid=0。

Dockerfile定义一个用户

当我在Dockerfile中设置一个用户,并且使用这个用户启动容器,会发生什么呢?为了简化这个例子,我当前也不是一个特殊的gid,我使用marc这个用户来运行命令,当前的uid=1001。

1
2
marc@server:~$ echo $UID
1001

编写Dockerfile

1
2
3
4
FROM ubuntu:latest
RUN useradd -r -u 1001 -g appuser appuser
USER appuser
ENTRYPOINT [“sleep”, “infinity”]

接下来构建运行这个容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

发送了什么呢?我使用appuser编译一个镜像,但是他有一个特殊的uid=1001,这和我当前用户marc的uid是一样的。当我启动这个容器有,sleep命令被appuser启动了,因为我的Dockerfile中指定了USER appuser。但是,这并不是就说明了他是使用的appuser这个用户在运行,Docker镜像中这个uid的用户被认为是appuser。当我检查容器之外的进程的时候,他显示的的macr用户。连个不同的用户名,在容器外和容器都被映射到了同一个uid=1001。

如何控制容器的权限

另一个话题是指定特殊的uid和gid来运行容器,再起看如下例子:

1
2
3
4
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

我在这儿做了什么?我使用uid=1001的用户启动了一个容器,所以当我在使用top,ps等工具的时候,这个用户被映射到marc用户了。有趣的是,我在容器里面执行命令的时候显示没有用户名了。所以说名1001这个用户在/etc/passwd中不存在。

1
2
marc@server:~$ docker exec -it 84f43 /bin/bash
I have no name!@84f436065c90:/$

有一个非常要说明的是:当使用docker run的启动参数指定用户的时候,会覆盖Dockerfile指定的用户。还记得上面第二个例子,使用相同的uid显示了不同的用户名的例子吗。现在我们使用-user选项来启动容器会发生什么呢?

1
2
3
4
5
6
7
8
9
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

你可以看到两个容器,一个是展示的是root,另一个展示的是marc。

说明了什么

现在我们发现,容器中的用户权限已经逃离了宿主的机器的权限控制了:

  • 如果我们知道容器中进程的uid,最简单的方式是控制宿主机器上相同uid的用户的权限。
  • 更好的方式是在docker run 的时候指定用户,可以是指定用户名,也可以是指定uid,最好是指定uid。然后在宿主机器上控制对应用户的权限。
  • 容器中的uid,gid和容器外的相同uid,gid有相同的权限

最后说明

上面我演示的例子是使用的ubuntu 16.04系统,可能你使用Mac OSX的时候会有不同额显示,因为Docker for Mac本身是运行在一个Alpine Linux

原文地址: https://medium.com/@mccode/understanding-how-uid-and-gid-work-in-docker-containers-c37a01d01cf