默认情况下,容器中的进程以 root 用户许可权运行,并且这个 root 用户和宿主机中的 root 是同一个用户。听起来是不是很可怕,因为这就意味著一旦容器中的进程有了适当的机会,它就可以控制宿主机上的一切!本文我们将尝试了解用户名、组名、用户 id(uid)和组 id(gid)如何在容器内的进程和主机系统之间映射,这对于系统的安全来说是非常重要的。说明:本文的演示环境为 ubuntu 16.04(下图来自互联网)。
uid 和 gid 由 Linux 内核负责管理,并通过内核级别的系统调用来决定是否应该为某个请求授予特权。比如当进程试图写入文件时,内核会检查创建进程的 uid 和 gid,以确定它是否有足够的许可权修改文件。注意,内核使用的是 uid 和 gid,而不是用户名和组名。
很多同学简单地把 docker 容器理解为轻量的虚拟机,虽然这简化了理解容器技术的难度但是也容易带来很多的误解。事实上,与虚拟机技术不同:同一主机上运行的所有容器共享同一个内核(主机的内核)。容器化带来的巨大价值在于所有这些独立的容器(其实是进程)可以共享一个内核。这意味著即使由成百上千的容器运行在 docker 宿主机上,但内核控制的 uid 和 gid 则仍然只有一套。所以同一个 uid 在宿主机和容器中代表的是同一个用户(即便在不同的地方显示了不同的用户名)。
注意,由于普通的用来显示用户名的 Linux 工具并不属于内核(比如 id 等命令),所以我们可能会看到同一个 uid 在不同的容器中显示为不同的用户名。但是对于相同的 uid 不能有不同的特权,即使在不同的容器中也是如此。
如果你已经了解了 Linux 的 user namespace 技术,参考《Linux Namespace : User》,你需要注意的是到目前为止,docker 默认并没有启用 user namesapce,这也是本文讨论的情况。笔者会在接下来的文章中介绍如何配置 docker 启用 user namespace。
如果不做相关的设置,容器中的进程默认以 root 用户许可权启动,下面的 demo 使用 ubuntu 镜像运行 sleep 程序:
$ docker run -d --name sleepme ubuntu sleep infinity0
注意上面的命令中并没有使用 sudo。笔者在宿主机中的登录用户是 nick,uid 为 1000:
在宿主机中查看 sleep 进程的信息:
$ ps aux | grep sleep
sleep 进程的有效用户名称是 root,也就是说 sleep 进程具有 root 许可权。
其实我们可以通过数据卷来简单的验证上面的结论。在宿主机上创建一个只有 root 用户可以读写的文件
然后挂载到容器中:
$ docker run --rm -it -w=/testv -v $(pwd)/testv:/testv ubuntu
在容器中可以读写该文件
我们可以在 Dockerfile 中添加一个用户 appuser,并使用 USER 命令指定以该用户的身份运行程序,Dockerfile 的内容如下:
FROM ubuntu RUN useradd -r -u 1000 -g appuser USER appuser ENTRYPOINT ["sleep", "infinity"]
编译成名称为 test 的镜像:
$ docker build -t test
用 test 镜像启动一个容器:
$ docker run -d --name sleepme test.
这次显示的有效用户是 nick,这是因为在宿主机中,uid 为 1000 的用户的名称为 nick。再进入到容器中看看:
$ docker exec -it sleepme bash
让我们再创建一个只有用户 nick 可以读写的文件:
$ docker run -d --name sleepme -w=/testv -v $(pwd)/testv:/testv test
在容器中 testfile 的所有者居然变成了 appuser,当然 appuser 也就有许可权读写该文件。
这里到底发生了什么?而这些又这说明了什么?
我们还可以通过 docker run 命令的 --user 参数指定容器中进程的用户身份。比如执行下面的命令:
$ docker run -d --user 1000 --name sleepme ubuntu sleep infinity
因为我们在命令行上指令了参数 --user 1000,所以这里 sleep 进程的有效用户显示为 nick。进入到容器内部看一下:
这是个什么情况?用户名称居然显示为 "I have no name!"!去查看 /etc/passwd 文件,里面果然没有 uid 为 1000 的用户。即便没有用户名称,也丝毫不影响该用户身份的许可权,它依然可以读写只有 nick 用户才能读写的文件,并且用户信息也由 uid 代替了用户名:
需要注意的是,在创建容器时通过 docker run --user 指定的用户身份会覆盖掉 Dockerfile 中指定的值。
$ docker run -d test
查看 sleep 进程信息:
$ docker run --user 0 -d test
再次查看 sleep 进程信息:
指定了 --urser 0 参数的进程显示有效用户为 root,说明命令行参数 --user 0 覆盖掉了 Dockerfile 中 USER 命令的设置。
从本文中的示例我们可以了解到,容器中运行的进程同样具有访问主机资源的许可权(docker 默认并没有对用户进行隔离),当然一般情况下容器技术会把容器中进程的可见资源封锁在容器中。但是通过我们演示的对数据卷中文件的操作可以看出,一旦容器中的进程有机会访问到宿主机的资源,它的许可权和宿主机上用户的许可权是一样的。所以比较安全的做法是为容器中的进程指定一个具有合适许可权的用户,而不要使用默认的 root 用户。当然还有更好的方案,就是应用 Linux 的 user namespace 技术隔离用户,笔者会在接下来的文章中介绍如何配置 docker 开启 user namespace 的支持。
参考:
作者:sparkdev出处:http://t.cn/Eflqt47
作者:sparkdev
51Reboot 2019 最新课程招生信息
Python 零基础入门课程此课程为面授班和网路班,一共 15 个课时,每周上一个全天,历时4个月。附加:录播视频+笔记+答疑2019-6月份开课
Python 自动化运维进阶课程此课程为面授班和网路班,一共 15 个课时,每周上一个全天,历时4个月。附加:录播视频+笔记+答疑2019-4月份开课
Golang 课程
现在报名即可享受早鸟价
对此感兴趣的朋友可以留言或私信我
推荐阅读: