笔者在前文《理解 docker 容器中的 uid 和 gid》介绍了 docker 容器中的用户与宿主机上用户的关系,得出的结论是:docker 默认没有隔离宿主机用户和容器中的用户。如果你已经了解了 Linux 的 user namespace 技术(参考《Linux Namespace : User》),那么自然会问:docker 为什么不利用 Linux user namespace 实现用户的隔离呢?事实上,docker 已经实现了相关的功能,只是默认没有启用而已。笔者将在本文中介绍如何配置 docker 来隔离容器中的用户。
说明:本文的演示环境为 ubuntu 16.04。

了解 Linux user namespace

Linux user namespace 为正在运行的进程提供安全相关的隔离(其中包括 uid 和 gid),限制它们对系统资源的访问,而这些进程却感觉不到这些限制的存在。关于 Linux User Namespace 的介绍请参考笔者的《Linux Namespace : User》一文。

对于容器而言,阻止权限提升攻击(privilege-escalation attacks)的最好方法就是使用普通用户权限运行容器的应用程序。
然而有些应用必须在容器中以 root 用户来运行,这就是我们使用 user namespace 的最佳场景。我们通过 user namespace 技术,把宿主机中的一个普通用户(只有普通权限的用户)映射到容器中的 root 用户。在容器中,该用户在自己的 user namespace 中认为自己就是 root,也具有 root 的各种权限,但是对于宿主机上的资源,它只有很有限的访问权限(普通用户)。

User namespace 的用户映射

在配置 docker daemon 启用 user namespace 前,我需要先来了解一些关于从属(subordinate)用户/组和映射(remapping)的概念。从属用户和组的映射由两个配置文件来控制,分别是 /etc/subuid 和 /etc/subgid。看下它们的默认内容:在配置 docker daemon 启用 user namespace 前,我需要先来了解一些关于从属(subordinate)用户/组和映射(remapping)的概念:

对于 subuid,这一行记录的含义为:
用户 nick,在当前的 user namespace 中具有 65536 个从属用户,用户 ID 为 100000-165535,在一个子 user namespace 中,这些从属用户被映射成 ID 为 0-65535 的用户。subgid 的含义和 subuid 相同。

比如说用户 nick 在宿主机上只是一个具有普通权限的用户。我们可以把他的一个从属 ID(比如 100000 )分配给容器所属的 user namespace,并把 ID 100000 映射到该 user namespace 中的 uid 0。此时即便容器中的进程具有 root 权限,但也仅仅是在容器所在的 user namespace 中,一旦到了宿主机中,你顶多也就有 nick 用户的权限而已。

当开启 docker 对 user namespace 的支持时(docker 的 userns-remap 功能),我们可以指定不同的用户映射到容器中。比如我们专门创建一个用户 dockeruser,然后手动设置其 subuid 和 subgid:

nick::
dockeruser::

并把它指定给 docker daemon:

{
"userns-remap": "dockeruser"
}

请注意 subuid 的设置信息,我们为 dockeruser 设置的从属 ID 和 nick 用户是不重叠的,实际上任何用户的从属 ID 设置都是不能重叠的。

或者一切从简,让 docker 为我们包办这些繁琐的事情,直接把 docker daemon 的 userns-rempa 参数指定为 "default":

{
"userns-remap": "default"
}

这时,docker 会自动完成其它的配置。

配置 docker daemon 启用用户隔离

这里笔者采取简单的方式,让 docker 创建默认的用户用于 user namespace。我们需要先创建 /etc/docker/daemon.json 文件:

$ sudo touch /etc/docker/daemon.json

然后编辑其内容如下(如果该文件已经存在,仅添加下面的配置项即可),并重启 docker 服务:

{
"userns-remap": "default"
}
$ sudo systemctl restart docker.service

下面我们来验证几个关于用户隔离的几个点。

首先验证 docker 创建了一个名为 dockremap 的用户:

然后查看 /etc/subuid 和 /etc/subgid 文件中是否添加了新用户 dockremap 相关的项:

接下来我们发现在 /var/lib/docker 目录下新建了一个目录: 165536.165536,查看该目录的权限:

165536 是由用户 dockremap 映射出来的一个 uid。查看 165536.165536 目录的内容:

与  /var/lib/docker 目录下的内容基本一致,说明启用用户隔离后文件相关的内容都会放在新建的 165536.165536 目录下。

通过上面的检查,我们可以确认 docker daemon 已经启用了用户隔离的功能。

宿主机中的 uid 与容器中 uid

在 docker daemon 启用了用户隔离的功能后,让我们看看宿主机中的 uid 与容器中 uid 的变化。

$ docker run -d --name sleepme ubuntu sleep infinity

uid 165536 是用户 dockremap 的一个从属 ID,在宿主机中并没有什么特殊权限。然而容器中的用户却是 root,这样的结果看上去很完美:

新创建的容器会创建 user namespace

在 docker daemon 启用用户隔离的功能前,新创建的容器进程和宿主机上的进程在相同的 user namespace 中。也就是说 docker 并没有为容器创建新的 user namespace:

上图中的容器进程 sleep 和宿主机上的进程在相同的 user namespace 中(没有开启用户隔离功能的场景)。

在 docker daemon 启用用户隔离的功能后,让我们查看容器中进程的 user namespace:

上图中的 4404 就是我们刚启动的容器中 sleep 进程的 PID。可以看出,docker 为容器创建了新的 user namespace。在这个 user namespace 中,容器中的用户 root 就是天神,拥有至高无上的权力!

访问数据卷中的文件

我们可以通过访问数据卷中的文件来证明容器中 root 用户究竟具有什么样的权限?创建四个文件,分别属于用户 root 、165536 和 nick。rootfile 只有 root 用户可以读写,用户 nick 具有 nickfile 的读写权限,uid 165536 具有文件 165536file 的读写权限,任何用户都可以读写 testfile 文件:

下面把这几个文件以数据卷的方式挂载到容器中,并检查从容器中访问它们的权限:

$ docker run -it --name test -w=/testv -v $(pwd)/testv:/testv ubuntu

容器中的 root 用户只能访问 165536file 和 testfile,说明这个用户在宿主机中只有非常有限的权限。

在容器中禁用 user namespace

一旦为 docker daemon 设置了 "userns-remap" 参数,所有的容器默认都会启用用户隔离的功能(默认创建一个新的 user namespace)。有些情况下我们可能需要回到没有开启用户隔离的场景,这时可以通过 --userns=host 参数为单个的容器禁用用户隔离功能。--userns=host 参数主要给下面三个命令使用:

docker container create
docker container run
docker container exec

比如执行下的命令:

$ docker run -d --userns=host --name sleepme ubuntu sleep infinity

查看进程信息:

进程的有效用户又成 root 了,并且也没有为进程创建新的 user namespace:

已知问题

User namespace 属于比较高级的功能,目前 docker 对它的支持还算不上完美,下面是已知的几个和现有功能不兼容的问题:

  • 共享主机的 PID 或 NET namespace(--pid=host or --network=host)
  • 外部的存储、数据卷驱动可能不兼容、不支持 user namespace
  • 使用 --privileged 而不指定 --userns=host

总结

Docker 是支持 user namespace 的,并且配置的方式也非常简便。在开启 user namespace 之后我们享受到了安全性的提升,但同时也会因为种种限制让其它的个别功能出现问题。这时我们需要作出选择,告别一刀切的决策,让合适的功能出现的合适的场景中。

参考:
Understanding how uid and gid work in Docker containers
Introduction to User Namespaces in Docker Engine
Isolate containers with a user namespace

隔离 docker 容器中的用户的更多相关文章

  1. 隔离 docker 容器中的用户-------分享链接

    https://www.cnblogs.com/sparkdev/p/9614326.html

  2. Docker容器中用户权限管理

    在Linux系统中有一部分知识非常重要,就是关于权限的管理控制:Linux系统的权限管理是由uid和gid负责,Linux系统会检查创建进程的uid和gid,以确定它是否有足够的权限修改文件,而非是通 ...

  3. Docker容器中运行ASP.NET Core

    在Linux和Windows的Docker容器中运行ASP.NET Core 译者序:其实过去这周我都在研究这方面的内容,结果周末有事没有来得及总结为文章,Scott Hanselman就捷足先登了. ...

  4. 在 docker 容器中捕获信号

    我们可能都使用过 docker stop 命令来停止正在运行的容器,有时可能会使用 docker kill 命令强行关闭容器或者把某个信号传递给容器中的进程.这些操作的本质都是通过从主机向容器发送信号 ...

  5. docker容器中Postgresql 数据库备份

    查看运行的容器: docker ps 进入目标容器: docker exec -u root -it 容器名 /bin/bash docker 中,以root用户,创建备份目录,直接执行如下命令, p ...

  6. 无需安装 vsftpd , 直接使用 FTP 来管理 docker 容器中的文件

    无图无真相,先放个效果图:     背景 使用 docker 来跑一些服务很方便,但是有的时候想管理容器里面的文件却很麻烦 -- 一般常规做法有3种: 通过数据卷或数据卷容器的方式 启动容器的时候时候 ...

  7. docker容器中oracle数据库导出dmp文件

    Oracle数据库安装在docker容器中 1首先查看容器 docker ps 2进入oracle容器 docker exec -it 7f0f3f2d4f88 /bin/bash 3导出整个库:这个 ...

  8. 在docker容器中编译hadoop 3.1.0

    在docker容器中编译hadoop 3.1.0 优点:docker安装好之后可以一键部署编译环境,不用担心各种库不兼容等问题,编译失败率低. Hadoop 3.1.0 的源代码目录下有一个 `sta ...

  9. docker_facenet_image在Docker容器中运行Facenet环境搭建

    对开发和运维人员来说,可能最梦寐以求的就是一次性地创建或配置,可以在任意环境.任意时间让应用正常运行.而Docker恰恰是可以实现这一终极目标的瑞士军刀. 具体来说,Docker在开发和运维过程中,具 ...

随机推荐

  1. javascript 数据类型 -- 检测

    一.前言 在上一篇博文中 Javascript 数据类型 -- 分类 中,我们梳理了 javascript 的基本类型和引用类型,并提到了一些冷知识.大概的知识框架如下: 这篇博文就讲一下在写代码的过 ...

  2. flagSet 使用

    var ( flagSet = flag.NewFlagSet("main", flag.ExitOnError) // 参数定义 version = flagSet.Bool(& ...

  3. Hive管理表分区的创建,数据导入,分区的删除操作

    Hive分区和传统数据库的分区的异同: 分区技术是处理大型数据集经常用到的方法.在Oracle中,分区表中的每个分区是一个独立的segment段对象,有多少个分区,就存在多少个相应的数据库对象.而在P ...

  4. VMware vsphere client报错问题

    今天出现一种情况,搞了很久,重装client都不行,原来很简单,防止再犯.     异常信息:"VirtualInfrastructure.Utils.ClientsXml"的类型 ...

  5. CentOS 编译安装 Nodejs (实测 笔记 Centos 7.3 + node 6.9.5)

    环境: 系统硬件:vmware vsphere (CPU:2*4核,内存2G,双网卡) 系统版本:CentOS-7.0-1406-x86_64-DVD.iso 安装步骤: 1.准备 1.1 显示系统版 ...

  6. 干掉safedog命令

    sc delete safedogguardcenter    shutdown -r -t 00 两条命令搞定

  7. 小程序----选择地理位置 ( wx.chooseLocation ) 和 获取地理位置 (wx.getSetting)

    问题来了:假如我第一次使用wx.chooseLocation()获取权限被拒绝,然后使用wx.getSetting()来重新获取权限该怎么做呢? 思路:wx.chooseLocation()有fail ...

  8. Android Studio 直播弹幕

    我只是搬运:https://blog.csdn.net/HighForehead/article/details/55520199 写的很好很详细,挺有参考价值的 demo直通车:https://do ...

  9. Unbuntu 16.04 英文环境安装中文输入法

    ubuntu 16.04 使用的是ibus输入系统,没有预装中文输入法,你要自己安装一下.以中文拼音输入法为例:1.sudo apt install ibus-pinyin2.sudo apt ins ...

  10. Java堆和栈的区别和介绍,JVM的堆和栈

    一.Java的堆内存和栈内存 Java把内存划分成两种:一种是堆内存,一种是栈内存.   堆:主要用于存储实例化的对象,数组.由JVM动态分配内存空间.一个JVM只有一个堆内存,线程是可以共享数据的. ...