本文仅作为学习记录,非商业用途,侵删,如需转载需作者同意。
学习了Linux capabilities 概念后,知道了对于非privileged 的容器,容器中root用户的capabilities 是有限制的,因此容器中的root用户无法像宿主机上的root用户一样,拿到完全掌控系统的特权。
这样是不是就安全了呢?
一、问题再现
Linux Namespace中有一项隔离技术,也就是 User Namespace。
不过在容器云平台kubernetes上目前还不支持User Namespace。那没有User Namespace的情况下,容器中用root用户运行会发生什么情况呢?
首先用下面的命令启动一个容器,在这里把宿主机上的/etc 目录以 volume 的形式挂载到了容器中的 /mnt 目录下。
# docker run -d --name root_example -v /etc:/mnt centos sleep 3600
然后我们可以看到容器中的进程 “sleep 3600” ,它在容器中和宿主机上的用户都是root,也就是说,容器中的用户 uid/gid 和宿主机上一样的。
# docker exec -it root_example bash -c "ps -ef | grep sleep"
root 1 0 0 01:14 ? 00:00:00 /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 3600
# ps -ef | grep sleep
root 5473 5443 0 18:14 ? 00:00:00 /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 3600
虽然容器中的root用户的capabilities 被限制了一些,但是在容器中,对于被挂载上来的 /etc 目录下的文件,比如说 shadow文件,以这个root 用户的权限还是可以做修改的。
# docker exec -it root_example bash
[root@9c7b76232c19 /]# ls /mnt/shadow -l
---------- 1 root root 586 Nov 26 13:47 /mnt/shadow
[root@9c7b76232c19 /]# echo "hello" >> /mnt/shadow
再去宿主机上查看下,是否修改了。
# tail -n 3 /etc/shadow
grafana:!!:18437::::::
tcpdump:!!:18592::::::
hello
这个例子说明容器中的root 用户也有权限修改宿主机上的关键文件。
云平台上是可以限制容器挂载宿主机的目录的。
但是容器和宿主机是共享Linux 内核的,一旦软件有漏洞,那么容器中以root 用户运行的进程就有机会去修改宿主机上的文件了。
二、问题分析
下面讨论下解决办法:
方法一:Run as non-root user(给容器指定一个普通用户)
我们如果不想让容器以root用户运行,最直接的办法就是给容器指定一个普通用户 uid。
1、比如在docker启动容器的时候加上"-u" 参数,在参数中指定 uid/gid。
# docker run -ti --name root_example -u 6667:6667 -v /etc:/mnt centos bash
bash-4.4$ id
uid=6667 gid=6667 groups=6667
bash-4.4$ ps -ef
UID PID PPID C STIME TTY TIME CMD
6667 1 0 1 01:27 pts/0 00:00:00 bash
6667 8 1 0 01:27 pts/0 00:00:00 ps -ef
2、创建容器镜像的时候,用Dockerfile 为容器镜像里建立一个用户
比如指定一个叫 nonroot 用户名的用户,启动容器中的进程。
这样运行Docker命令的时候就不用加 “-u” 参数来指定用户了。
# cat Dockerfile
FROM centos
RUN adduser -u 6667 nonroot
USER nonroot
# docker build -t registry/nonroot:v1 .
…
# docker run -d --name root_example -v /etc:/mnt registry/nonroot:v1 sleep 3600
050809a716ab0a9481a6dfe711b332f74800eff5fea8b4c483fa370b62b4b9b3
# docker exec -it root_example bash
[nonroot@050809a716ab /]$ id
uid=6667(nonroot) gid=6667(nonroot) groups=6667(nonroot)
[nonroot@050809a716ab /]$ ps -ef
UID PID PPID C STIME TTY TIME CMD
nonroot 1 0 0 01:43 ? 00:00:00 /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 3600
然后再看看是否可以修改挂载上来的目录的文件了:
[nonroot@050809a716ab /]$ echo "hello" >> /mnt/shadow
bash: /mnt/shadow: Permission denied
这样做,在云平台上还会带来其他的问题。
由于用户uid 是整个节点中共享的,那么在容器中定义的uid,也就是宿主机上的uid,这样很容易引起uid的冲突。
比如多个客户在建立自己的容器镜像的时候选择了同一个uid 6667,那么当多个客户的容器在同一个节点上运行的时候,其实使用的都是宿主机的uid 6667
PS:之前公司出现过宿主机上的docker进程僵死,docker报错打开文件数过多。
有些服务有问题,重启一下就好了。 怀疑和这个原因有些关系。
一台Linux 系统上,每个用户下的资源都是有限制的,比如打开文件数目(open files)、最大进程数目(max user processes)等。
一旦有很多个容器共享一个uid,这些容器很可能很快就消耗掉这个uid下的资源,这样很容易导致这些容器都不能正常工作了。
要解决这个问题,必须要有一个云平台级别的uid管理和分配。
用户在定义自己容器中的uid的时候,就需要有额外的操作,平台也需要新开发对uid平台级别的管理模块,完成这些事情需要的工作量也不少。
方法二、User Namespace(用户隔离技术的支持)
User Namespace 隔离了一台Linux节点上的User ID(uid)和Group ID (gid)。
它给Namespace中的uid/gid 的值与宿主机上的uid/gid的值建立了一个映射关系。经过User Namespace的隔离,我们在Namespace中看到的进程的uid/gid 就和宿主机Namespace中看到的uid和gid不一样了。
上图中 namespace_1 里的uid的值是0到999,它在宿主机上对应的uid的值是1000到1999。
User Namespace 是可以嵌套的,下图中的namespace_2 里可以再建立一个 namespace_3 ,这个嵌套的特性是其他Namespace 没有的。
==
下面使用podman这个工具,而不是docker。
跟Docker 相比,podman不再有守护进程dockerd,而是直接通过fork/execve的方式来启动一个新的容器,这种方式启动容器更加简单,也更容器维护。
podman的命令参数兼容了绝大部分的docker命令行参数。
podman 启动命令如下:
# podman run -ti -v /etc:/mnt --uidmap 0:2000:1000 centos bash
这里在命令中增加一个参数,"–uidmap 0:2000:1000",这个是标准的User Namespace中的uid映射格式:“ns_uid:host_uid:amount”
第一个0 指在新的Namespace 里 uid 从0开始,中间的2000指的是Host Namespace 里被映射的uid 从2000开始,最后一个1000 指需要连续映射1000个uid。
容器里的uid 0 是被映射到宿主机上的uid 2000的。
验证如下:
容器中使用 uid 0 运行一个命令:
# id
uid=0(root) gid=0(root) groups=0(root)
# sleep 3600
宿主机上查看这个进程的uid,进程uid的确是2000了。
# ps -ef |grep sleep
2000 27021 26957 0 01:32 pts/0 00:00:00 /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 3600
再回到容器中,容器中root用户对被挂载上来的/etc 目录下的文件做操作,是没有权限的。
# echo "hello" >> /mnt/shadow
bash: /mnt/shadow: Permission denied
# id
uid=0(root) gid=0(root) groups=0(root)
User Namespace的好处如下:
- 把容器中的root用户,映射成宿主机上的普通用户
作为容器中的root,它还是有一些 Linux capabilities,那么在容器中还是可以执行一些权限的。而在宿主机的uid是普通用户,即使这个用户逃逸出容器Namespace,它的执行权限还是有限的。
- 对于用户在容器中自己定义普通用户uid的情况,我们只要为每个容器在节点上分配一个uid范围,就不会出现在宿主机上uid冲突的问题了。
我们只要在节点上分配容器的uid范围就可以了。
相比较整个平台给容器分配uid,使用User Namespace 这个方法要方便的多。
kubernetes 目前还不支持User Namespace 。
想了解相关进展,可以关注这个PR
- rootless container (以非root启动和管理容器)
就是启动容器的时候,Docker 或者Podman 是以非root 用户来执行的。
这样能提示容器中的安全性,不用担心因为containerd或者RunC的漏洞,导致容器获取了宿主机上的权限。
下面一个例子:
redhat blog
在宿主机上用 redhat 这个用户通过 podman 来启动一个容器。在这个容器中也使用了 User Namespace,并且把容器中的 uid 0 映射为宿主机上的 redhat 用户了。
$ id
uid=1001(redhat) gid=1001(redhat) groups=1001(redhat)
$ podman run -it ubi7/ubi bash ### 在宿主机上以redhat用户启动容器
[root@206f6d5cb033 /]# id ### 容器中的用户是root
uid=0(root) gid=0(root) groups=0(root)
[root@206f6d5cb033 /]# sleep 3600 ### 在容器中启动一个sleep 进程
# ps -ef |grep sleep ###在宿主机上查看容器sleep进程对应的用户
redhat 29433 29410 0 05:14 pts/0 00:00:00 sleep 3600
目前 Docker 和 podman 都支持了 rootless container,Kubernetes 对rootless container 支持的工作也在进行中。
三、重点小结
尽管容器中root 用户的Linux capabilities 已经减少了很多,但是在没有User Namespace 的情况下,容器中的root用户和宿主机上的root用户的uid 是完全相同的,一旦有软件漏洞,容器中的root用户就可以操控整个宿主机。
为了减少安全风险,业界都是建议在容器中以非root用户来运行进程。
不过在没有User Namespace的情况下,在容器中使用非root用户,对于容器云平台来说,对uid的管理会比较麻烦。
User Namespace 的好处:
- 把容器中的root用户映射成宿主机上的普通用户,另外一个好处是在云平台kubernetes 这种,对于容器uid的分配要容易些。
除了在容器中以非root用户来运行进程外,Docker 和 Podman 都支持了 rootless container,也就是说他们都可以以非root用户来启动和管理容器,进一步降低了容器的安全风险。
四、评论
1、
问题:
最近在使用Helm部署gitlab服务的过程中,就发现了 postgresql 和 redis 组件默认是不以root用户执行的,而是一个 User ID 为1001的用户在执行
这样做,就需要有个k8s的 initContainer 容器先以root用户权限去修改存储目录的权限. 否则后面服务的1001号用户可能就没有权限去写文件了.
===
最近遇到一个问题,想咨询一下老师:
你们有使用过 容器资源可视化隔离方案(lxcfs) 么, 有没有什么坑?
通俗点就是:让容器中的free, top等命令看到容器的数据,而不是物理机的数据。
===
我遇到的问题是在容器内执行类似go build/test
命令时,默认是根据当前CPU核数来调整构建的并发数.
这就导致了实际只给容器分配了1个核,但是它以为自己有16个核.
然后就开16个link进程,互相之间除了有竞争,导致CPU上下文切换频繁,更要命的是把磁盘IO给弄满了.影响了整台宿主机的性能.
(由于项目比较大,需要构建的文件比较多,所以很容器就让宿主机的IO达到了云服务器SSD磁盘的限制 160MB/s)
我知道在我这个场景下,可以通过指定构建命令-p
来控制构建的并发数.
(https://golang.org/cmd/go/#hdr-Compile_packages_and_dependencies)
实际也这么尝试过,效果也不错.
但问题是,我的项目会很多,每个人构建命令的写法都完全不一样,如果每个地方都去指定参数,就会比较繁琐,且容易遗漏.
后来,我看到一篇文章: 容器资源可视化隔离的实现方法
(https://mp.weixin.qq.com/s/SCxD4OiDYsmoIyN5XMk4YA)
之前也在其他专栏中看老师提到过 lxcfs.
我在想,老师在迁移上k8s的过程中,肯定也遇到过类似的问题,不知道老师是如何解决的呢?
回答:
很好的问题。我们在最开始也考虑过使用lxcfs, 当时碰到的问题也是当一批java应用从虚拟机迁移到容器平台之后,发现jvm看到的是整个宿主机的资源。
不过后来,发现大部分语言和应用都是可以加参数或者做修改来适配容器化的,因此,我们的方向是让应用也必须考虑容器和云原生的设计,因为这个是大的趋势,应用这边也是愿意接受这个改变的。
还有一点,当时我们在试lxcfs的时候发现,如果容器需要的cpu不是整数,似乎lxcfs也不能支持(不知道最新的lxcfs是不是有所改变),同时在host上需要额外维护这个lxcfs的可靠性。 这样在大部分主要应用都愿意往容器化方向走的大环境下,我们就不再考虑lxcfs了。
2、
问题:
老师 docker -u 参数 是不是就是 通过user namespace 进行隔离
回答:
-u 只是指定了在容器启动的时候缺省用的uid/gid, 这里的uid/gid和宿主机上的是一样的,并没有建立出新的user namespace
3、
问题:
user limit 是session的?每个容器及时使用相同的user id ,也不会当做累计?
User resource limits dictate the amount of resources that can be used for a particular session. The resources that can be controled are:
maximum size of core files
maximum size of a process’s data segment
maximum size of files created
maximum size that may be locked into memory
maximum size of resident memory
maximum number of file descriptors open at one time
maximum size of the stack
maximum amount of cpu time used
maximum number of processes allowed
maximum size of virtual memory available
It is important to note that these settings are per-session. This means that they are only effective for the time that the user is logged in (and for any processes that they run during that period). They are not global settings. In other words, they are only active for the duration of the session and the settings are not cumulative. For example, if you set the maximum number of processes to 11, the user may only have 11 processes running per session. They are not limited to 11 total processes on the machine as they may initiate another session. Each of the settings are per process settings during the session, with the exception of the maximum number of processes.
回答:
我在这里指的是pam_limits, 在/etc/security/limits.conf中限制某个用户资源之后,然后在pam *_auth 和 runuser中enable pam_limits 之后,那么同一个用户即使在不同的session里,资源的限制也是累计了。
你可以在CentOS的系统里试试。