了解用户名,组名,用户ID(uid)和组ID(gid)如何在容器内运行的进程与主机系统之间进行映射对于构建安全系统非常重要。如果没有提供任何其他选项,容器中的进程将以root身份执行(除非在Dockerfile中提供了不同的uid)。本文将解释这是如何工作的,如何正确授予权限并显示示例来说明。
逐步分析uid/gid安全性
首先,让我们回顾一下如何实现uid和gid。linux内核负责管理uid和gid空间,它的内核级系统调用用于确定是否应该授予所请求的权限。例如,当进程尝试写入文件时,内核会检查创建该进程的uid和gid,以确定它是否具有足够的权限来修改该文件。这里不使用用户名,使用uid。
在服务器上运行Docker容器时,仍然只有一个内核。容器化带来的巨大价值在于所有这些独立的进程可以继续共享单个内核。这意味着即使在运行Docker容器的服务器上,整个uids和gids也由单个内核控制。
所以你不能让不同的用户在不同的容器中使用相同的uid。这是因为在常见的linux工具中显示的用户名(和组名)不是内核的一部分,而是由外部工具(/etc/passwd,LDAP,Kerberos等)管理。因此,你可能会看到不同的用户名,但即使在不同的容器内,你也无法为同一个uid/gid拥有不同的权限。这一开始看起来很混乱,所以让我们用几个例子说明一下:
简单的Docker运行
我将首先以docker组中的普通用户(linuxea)身份登录服务器。这允许我在不使用sudo命令的情况下启动docker容器。然后,从容器外部,让我们看看这个过程是如何出现的。
[root@www-linuxea-com ~]# useradd linuxea -G docker
[linuxea@www-linuxea-com ~]# su - linuxea
[linuxea@www-linuxea-com ~]$ docker run -d alpine:3.9 sleep 10000
e18b6e93b21a30f095ca0c6cbfbd0762dcbc220b3c057a7233c5371fe4f40c47
[linuxea@www-linuxea-com ~]$ ps aux|grep sleep
root 7145 0.5 0.0 1520 4 ? Ss 15:57 0:00 sleep 10000
linuxea 7203 0.0 0.1 112716 2192 pts/0 S+ 15:58 0:00 grep --color=auto sleep
这很有趣。即使我从未输入sudo并且我不是root用户,我执行的sleep命令也以root用户身份启动并具有root权限。我怎么知道它有root权限?容器内的root = =容器外的root吗?
是的,因为,正如我所提到的,有一个内核和一个共享的uids和gids池。因为用户名在容器外部显示为“root”,所以我可以肯定地知道容器内的进程是由具有uid = 0的用户启动的。
定义用户的Dockerfile
当我在Dockerfile中创建不同的用户并以该用户身份启动命令时会发生什么?为了简化这个例子,我没有在这里指定一个gid,但同样的概念适用于组ID。
首先,我将这些命令作为用户“linuxea”运行,其uid为1001。
[linuxea@www-linuxea-com ~]$ echo $UID
1001
Dockerfile
[linuxea@www-linuxea-com ~]$ cat Dockerfile
FROM alpine:latest
MAINTAINER www.linuxea.com mark
RUN addgroup www && adduser -u 1001 -S -H -s /sbin/nologin -g 'nginx' -G www www
USER www
ENTRYPOINT ["sleep","30000"]
让我们构建并运行它:
[linuxea@www-linuxea-com ~]$ docker build -t test .
Sending build context to Docker daemon 6.656kB
Step 1/5 : FROM alpine:latest
---> caf27325b298
Step 2/5 : MAINTAINER www.linuxea.com mark
---> Using cache
---> 97c60ecc6438
Step 3/5 : RUN addgroup www && adduser -u 1001 -S -H -s /sbin/nologin -g 'nginx' -G www www
---> Running in dd3eaaa2d820
Removing intermediate container dd3eaaa2d820
---> 1a81d642cab6
Step 4/5 : USER www
---> Running in fedfcda0d798
Removing intermediate container fedfcda0d798
---> fc564057218b
Step 5/5 : ENTRYPOINT ["sleep","30000"]
---> Running in a87dfbe5edd8
Removing intermediate container a87dfbe5edd8
---> 0b28fd7c7ad1
Successfully built 0b28fd7c7ad1
Successfully tagged test:latest
[linuxea@www-linuxea-com ~]$ docker run -d test
06494798ce210032582e21a2b3f990fcdfe3e9623402c41bd78f4c22fa5ce518
[linuxea@www-linuxea-com ~]$ ps aux|grep sleep
linuxea 10652 0.1 0.0 1520 4 ? Ss 16:42 0:00 sleep 30000
linuxea 10716 0.0 0.1 112716 2144 pts/0 S+ 16:42 0:00 grep --color=auto sleep
[linuxea@www-linuxea-com ~]$ docker exec -i 0649 ps aux|grep sleep
1 www 0:00 sleep 30000
?
到底发生了什么,这表明了什么?我构建了一个Docker镜像,其用户名为“www”,该用户的定义uid为1001.在我的测试服务器上,我正在使用的帐户名为“linuxea”,它的uid为1001。我启动容器,sleep命令作为www执行,因为Dockerfile包含“ADDUSER www”行。但这实际上并没有使它作为www运行,它使它作为用户的uid运行,Docker镜像是已知www用户是自己的用户的。
当我检查在容器外部运行的进程时,我看到它被映射到用户“linuxea”,但是在容器内部它被映射到用户“www”。这两个用户名只显示其执行上下文已知的用户名映射到1001。
这不是非常重要的。但重要的是要知道在容器内部,用户“www”从容器外获取用户“linuxea”的权限和特权。在linux主机上向用户linuxea或uid 1001授予权限也将授予容器内www的这些权限。
如何控制容器的访问权限
另一种选择是运行docker容器并指定用户名或uid,以及运行时的组名或gid。
再次使用上面的初始示例。
[linuxea@www-linuxea-com ~]$ docker run -d --user 1001 alpine:3.9 sleep 30001
b197a816bdc0a71060331e2b5a6701432cce786161e2d9939dd3a50eabf0fa9b
[linuxea@www-linuxea-com ~]$ ps aux|grep sleep
linuxea 12071 0.7 0.0 1520 4 ? Ss 17:01 0:00 sleep 30001
linuxea 12131 0.0 0.1 112716 2216 pts/0 S+ 17:01 0:00 grep --color=auto sleep
我在这做什么?
我创建了容器以作为1001用户启动。因此,当我执行诸如ps或top(或大多数监视工具)之类的命令时,该过程将映射到“linuxea”用户。
有趣的是,当我执行到该容器时,你可以看到1001用户在/etc/passwd文件中没有条目,并在容器的bash提示符中显示为“I have no name!”。
现在我使用ubuntu
[linuxea@www-linuxea-com ~]$ docker run -d --user 1001 ubuntu:19.04 sleep 30001
a658a0a5b49db14831d83b792413dc0cc08112877c64ffb2006c4c0e716a83f4
[linuxea@www-linuxea-com ~]$ docker exec -it a65 bash
I have no name!@a658a0a5b49d:/$
需要注意的是,在创建容器时指定用户标志也会覆盖Dockerfile中的该值。还记得第二个例子,我使用的Dockerfile有一个映射到本地主机上不同用户名的uid吗?当我们在命令行上使用用户标志运行它以启动执行“sleep 30000”过程的容器时会发生什么?
[linuxea@www-linuxea-com ~]$ docker run -d test
25a0cdc509b2d56185fd42046249d2b53c3a84e393f76c1ca304e552b5ab1058
[linuxea@www-linuxea-com ~]$ ps aux|grep sleep
linuxea 14157 0.6 0.0 1520 4 ? Ss 17:18 0:00 sleep 30000
linuxea 14215 0.0 0.1 112716 2144 pts/1 S+ 17:18 0:00 grep --color=auto sleep
[linuxea@www-linuxea-com ~]$ docker run -d --user 0 test
5dc20cacd8d8c075c3e0ba910a72b36c16b851ef9a7ef56a4d90a2c97d148dc6
[linuxea@www-linuxea-com ~]$ ps aux|grep sleep
linuxea 14157 0.1 0.0 1520 4 ? Ss 17:18 0:00 sleep 30000
root 14275 3.0 0.0 1520 4 ? Ss 17:18 0:00 sleep 30000
linuxea 14330 0.0 0.1 112716 2216 pts/1 S+ 17:18 0:00 grep --color=auto sleep
在上面的最后一个例子中,你可以看到我最终有2个容器运行睡眠过程,一个用作“linuxea”,另一个用作“root”。这是因为第二个命令通过--user
在命令行上传递标志来更改uid 。
权限测试
为了更好的验证这些,我们来测试一下。众所周知,普通用户是无法删除root用户创建的文件的
[linuxea@www-linuxea-com /opt]$ rm -rf 2
rm: cannot remove ‘2’: Permission denied
如果此时,我们将这些文件挂载到容器内,请注意,我通过不同的用户ID挂载后,进行删除
- user=0
[linuxea@www-linuxea-com ~]$ docker run -d --user=0 -v /opt:/opt test
[linuxea@www-linuxea-com ~]$ docker exec -it modest_newton sh
/ # cd /opt/
/opt # ls
1 2 2019
/opt # rm -rf 1
/opt # ls
2 2019
/opt #
使用--user=0后进行挂载,可以顺利删除root创建的目录
- user=1001
[linuxea@www-linuxea-com /opt]$ docker run -d --user=1001 -v /opt:/opt test
a4663d98c6e89f5c4af0f34516f0c4efe48a6fa881af881da5d8fd4029d9ccf8
[linuxea@www-linuxea-com /opt]$ docker exec -it a46 sh
/ $ cd /opt/
/opt $ ls
2 2019
/opt $ ls -l
total 4
-rw-r--r-- 1 root root 3784 Feb 15 06:44 2
drwxr-xr-x 4 root root 174 Feb 19 02:35 2019
/opt $ rm -rf 2
rm: can't remove '2': Permission denied
显而易见,单单从上述来看,普通用户似乎更为安全
为了改善这一点,我们可以使用券权限来解决,仍然使用--user=0,挂在的/opt权限是ro,这样就不能被删除了。如下:
[linuxea@www-linuxea-com ~]$ docker run -d --user=0 -v /opt:/opt:ro test
ac8cc54d81dec6b528519c85f845c15c6b8515ab2777dff6d0a51cae59350ad2
[linuxea@www-linuxea-com ~]$ docker exec -it ac8 sh
/ # cd /opt/
/opt # ls
2 2019
/opt # rm -rf 2
rm: can't remove '2': Read-only file system
/opt # ls -ll
total 4
-rw-r--r-- 1 www root 3784 Feb 15 06:44 2
drwxr-xr-x 4 root root 174 Feb 19 02:35 2019
/opt # rm -rf 2019
rm: can't remove '2019/1.sh': Read-only file system
rm: can't remove '2019/go1.11.4.linux-amd64.tar.gz': Read-only file system
rm: can't remove '2019/pipe.go': Read-only file system
rm: can't remove '2019/telnet.go': Read-only file system
rm: can't remove '2019/telnet.sh': Read-only file system
但是普通用户1001赋予rw,仍然无法删除
[linuxea@www-linuxea-com /opt]$ docker run -d --user=1001 -v /opt:/opt:rw test
a7cc54fd596df7b9f226d2633cd5fdffa5b6784469a2f6b37e944cdac0633db5
[linuxea@www-linuxea-com /opt]$ docker exec -it a7cc sh
/ $ cd /opt
/opt $ ls
2 2019
/opt $ ls -ll
total 4
-rw-r--r-- 1 www root 3784 Feb 15 06:44 2
drwxr-xr-x 4 root root 174 Feb 19 02:35 2019
/opt $ rm -rf 2
rm: can't remove '2': Permission denied
/opt $ rm -rf 2019
rm: can't remove '2019/1.sh': Permission denied
rm: can't remove '2019/go1.11.4.linux-amd64.tar.gz': Permission denied
rm: can't remove '2019/pipe.go': Permission denied
rm: can't remove '2019/telnet.go': Permission denied
另外,我们还需要知道一点,尽管是root用户user=0,但是与宿主机的root用户权限还是有与别的,突破这一点,我们需要使用特权模式--privileged
/opt # ls -ll /dev
total 0
lrwxrwxrwx 1 root root 11 Feb 20 10:01 core -> /proc/kcore
lrwxrwxrwx 1 root root 13 Feb 20 10:01 fd -> /proc/self/fd
crw-rw-rw- 1 root root 1, 7 Feb 20 10:01 full
drwxrwxrwt 2 root root 40 Feb 20 10:01 mqueue
crw-rw-rw- 1 root root 1, 3 Feb 20 10:01 null
lrwxrwxrwx 1 root root 8 Feb 20 10:01 ptmx -> pts/ptmx
drwxr-xr-x 2 root root 0 Feb 20 10:01 pts
crw-rw-rw- 1 root root 1, 8 Feb 20 10:01 random
drwxrwxrwt 2 root root 40 Feb 20 10:01 shm
lrwxrwxrwx 1 root root 15 Feb 20 10:01 stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 Feb 20 10:01 stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 Feb 20 10:01 stdout -> /proc/self/fd/1
crw-rw-rw- 1 root root 5, 0 Feb 20 10:01 tty
crw-rw-rw- 1 root root 1, 9 Feb 20 10:01 urandom
crw-rw-rw- 1 root root 1, 5 Feb 20 10:01 zero
而宿主机
[linuxea@www-linuxea-com /opt]$ ls /dev
autofs cuse fuse lightnvm network_latency raw tty tty16 tty24 tty32 tty40 tty49 tty57 tty8
block disk hidraw0 log network_throughput rtc tty0 tty17 tty25 tty33 tty41 tty5 tty58 tty9
btrfs-control dm-0 hpet loop-control null rtc0 tty1 tty18 tty26 tty34 tty42 tty50 tty59 ttyS0
bus dm-1 hugepages mapper nvram shm tty10 tty19 tty27 tty35 tty43 tty51 tty6 ttyS1
char dm-2 hwrng mcelog port snapshot tty11 tty2 tty28 tty36 tty44 tty52 tty60 ttyS2
console dri initctl mem ppp snd tty12 tty20 tty29 tty37 tty45 tty53 tty61 ttyS3
core fb0 input memory_bandwidth ptmx stderr tty13 tty21 tty3 tty38 tty46 tty54 tty62 uhid
cpu fd kmsg mqueue pts stdin tty14 tty22 tty30 tty39 tty47 tty55 tty63 uinput
cpu_dma_latency full LBVG net random stdout tty15 tty23 tty31 tty4 tty48 tty56 tty7 urando
这意味着什么
现在我们已经探讨了这一点,有意义的是,运行具有有限权限的容器的方法都利用来自主机的用户系统:
--user
(你也可以使用用户名,但请记住,它只是从主机的用户名系统提供uid的更友好的方式),然后限制对主机上的uid的访问你决定容器将运行为。注意这些测试
我在Centos7服务器上运行这些测试。涉及的镜像有Ubuntu 19.04和alpine:3.9 。虽然可以在Docker for OSX上执行相同的命令和测试,但你会看到不同的结果,因为Docker for OSX实际上是在基于Alpine Linux的虚拟机上执行docker-engine,并且你执行的命令是在OSX上执行。
学习更多
学习如何使用Docker CLI命令,Dockerfile命令,使用这些命令可以帮助你更有效地使用Docker应用程序。查看Docker文档和我的其他帖子以了解更多信息。
- docker目录
- 白话容器
- docker-compose