默认情况下,容器中的进程以root用户权限运行,这个root用户与宿主机中的root是同一个用户。听起来很可怕,因为这意味着一旦容器中的进程有合适的机会,它就可以控制宿主机上的一切!在本文中,我们将尝试了解用户名、组名、用户id(uid)和组id(gid)是如何在容器内部进程和宿主系统之间进行映射的,这对系统的安全非常重要。注:本文演示环境为Ubuntu16.04(下图来源于网络)。我们先了解uid和giduid和gid是由Linux内核管理的,通过内核级的系统调用来决定一个请求是否应该被授予权限。例如,当一个进程试图写入一个文件时,内核会检查创建进程的uid和gid以查看它是否有足够的权限来修改文件。请注意,内核使用**uid和gid****而不是用户名和组名**。为了简单起见,本文其余部分仅以uid为例。系统对待gid的方式与uid基本相同。很多同学简单的把docker容器理解为一个轻量级的虚拟机。这虽然简化了容器技术的理解难度,但也容易造成很多误解。事实上,与虚拟机技术不同的是:运行在同一台主机上的所有容器共享同一个内核(主机的内核)。容器化带来的巨大价值在于所有这些独立的容器(实际上是进程)可以共享一个内核。这意味着即使在docker主机上运行了数百个容器,**kernel控制的**uid和gid**仍然只有一套。所以同一个uid在宿主机和容器中代表同一个用户(即使不同的地方显示不同的用户名)。注意,由于Linux常用的显示用户名的工具并不属于内核(比如id等命令),所以我们可能会看到同一个uid在不同的容器中显示为不同的用户名。但是你不能对同一个uid有不同的权限,即使在不同的容器中也是如此。如果你已经了解Linux的用户命名空间技术,参考《Linux Namespace :User》,需要注意的是到目前为止,docker默认是没有开启用户命名空间的,这也是本文讨论的情况。笔者将在下一篇文章中介绍如何配置docker开启usernamespace。默认情况下,容器中使用**root**用户。如果不做相关设置,容器内的进程默认以root用户权限启动。下面的demo使用ubuntu镜像运行sleep程序:$dockerrun-d--namesleepmeubuntusleepinfinity注意上面的命令中并没有使用sudo。作者在宿主机中的登录用户是nick,uid是1000:查看宿主机中sleep进程的信息:$psaux|grepsleepssleep进程的有效用户名是root,也就是说sleep进程有root权威。然后进入容器内部,看到情况和之前一样,sleep进程也有root权限:那么,容器中的root用户和宿主机上的root用户是一样的吗?答案是:是的,它们对应同一个uid。原因我们前面已经解释过了:整个系统共享同一个内核,内核只管理一组uid和gid。其实我们可以简单的通过数据量来验证以上结论。在宿主机上创建一个只有root用户可以读写的文件:然后挂载到容器中:$dockerrun--rm-it-w=/testv-v$(pwd)/testv:/testvubuntu可以读入容器写入文件:我们可以通过Dockerfile中的USER命令或者dockerrun命令的--user参数来指定容器中进程的用户身份。下面我们分别考察这两种情况。在**Dockerfile**中指定用户身份我们可以在Dockerfile中添加一个用户appuser,使用USER命令指定以该用户身份运行程序。Dockerfile的内容如下:FROMubuntuRUNuseradd-r-u1000-gappuserUSERappuserENTRYPOINT\["sleep","infinity"\]编译成名为test的镜像:$dockerbuild-ttest。启动一个带有测试镜像的容器:$dockerrun-d--namesleepmetest查看宿主机中sleep进程的信息:这次显示的有效用户是nick,因为在宿主机中,uid为1000的用户名为nick.然后进入容器查看:$dockerexec-itsleepmebash容器中的当前用户就是我们设置的appuser。查看容器中的/etc/passwd文件,会发现appuser的uid为1000,与宿主机中的用户nick相同。uid是一样的。让我们创建另一个只有用户nick可以读写的文件:也将其作为数据卷挂载到容器中:$dockerrun-d--namesleepme-w=/testv-v$(pwd)/testv:/*的所有者testvtest容器中的*testfile其实变成了appuser**,当然appuser也有读写文件的权限。这到底是怎么回事?这是什么意思?首先,在主机系统中有一个uid为1000的用户nick。其次,容器中的程序以appuser运行,在Dockerfile程序中通过USERappuser命令指定。实际上,系统内核管理的uid1000只有一个。在宿主机中,它被认为是用户nick,而在容器中,它被认为是用户appuser。所以有一点我们需要明确一点:在容器内部,用户appuser可以获得容器外用户nick的权限。在主机上授予用户nick或uid1000的权限也将授予容器内的appuser。从命令行参数自定义用户身份我们也可以通过dockerrun命令的--user参数来指定容器中进程的用户身份。例如执行如下命令:$dockerrun-d--user1000--namesleepmeubuntusleepinfinity因为我们在命令行指定了参数--user1000,所以这里sleep进程的有效用户显示为nick。进入容器内部看看:$dockerexec-itsleepmebash是什么情况?用户名实际上说“我没有名字!”!查看/etc/passwd文件,里面没有uid为1000的用户。即使没有用户名,也丝毫不影响用户身份的权限。它仍然可以读写只有nick用户可以读写的文件,用户信息用uid代替用户名:需要注意的是,在创建容器时,通过dockerrun指定的用户身份——-user将覆盖Dockerfile中指定的值。我们通过测试镜像重新运行两个容器:$dockerrun-dtest查看sleep进程信息:$dockerrun--user0-dtest再次查看sleep进程信息:指定--urser0参数的进程显示有效用户为root,表示命令行参数--user0覆盖了Dockerfile中USER命令的设置。小结从本文的例子中我们可以了解到,运行在容器中的进程也可以访问宿主机资源(docker默认不隔离用户)。当然,一般情况下,容器技术会在容器中屏蔽容器中进程的可见资源。但是从我们演示的对数据卷中的文件的操作可以看出,容器中的进程一旦有机会访问宿主机的资源,它的权限就和宿主机上的用户是一样的。所以为容器中的进程指定一个具有适当权限的用户比使用默认的root用户更安全。当然,还有更好的解决方案,就是利用Linux用户命名空间技术对用户进行隔离。笔者将在下一篇文章中介绍如何配置docker开启用户命名空间支持。
