Docker 容器以及其所运行的宿主机的安全防护是维护可信的服务的关键。 以专业的安全领域的DevOps角度来看, 对container 以及container 的调度平台想OpenShift、Docker Swarm以及如日中天的K8S进行安全防护其实很简单。 主要是因为在技术革新如此快速的时代,目标变化的也如此频繁。

由于新技术的发展,其产生的许多挑战需要去解决,在解决这些问题的方法当中,有一项是有着重大意义,即在docker container当中对其宿主机的 UID和GID 映射到不同的值。

有一些没有威胁性的配置的变更,通过一个不是那么新的技术User Namespaces 可以将在Container当中root user 与Host的root user进行隔离。这一特性在2016年2月前后发布的Docker 1.10 版本中已经可以使用。之所以称其为“不是那么新的技术” 是因为任何一个从事容器化以及调度层系统开发的人知道,一个功能如果发布了6个月以上就会被认为是一个”古董“ ;)

实现细节

以下会通过一些实验来验证 host 级别,确切来说是内核级别的 User Namespace 安全防护方法

首先, 在谈到对Docker 容器或者其它平台的容器进行安全防防护时,有2个常见的相关联的术语。
也许你会想到的是Cgroup,cgroup 可以将进程在内核当中进行限定。我所说的限定指的是限制其能占有系统资源的能力,包括CPU,RAM 以及 IO

通过namespace 来控制一个进程其对系统资源的可视度并不会令你产生疑惑 。也许你并不想令某一个进程可以访问所有的网络协议栈或者其它在进程表当中的进程

在接下来的实验当中,会选取Docker 作为容器的运行时环境。本文会对user和group在host当中的container进行重新的映射。host指的时运行docker 后台守护进程的服务器。
更进一步,实验会通过改变容器进程对其资源的可视度来防护Host。

对user和group的重映射即对user namespace 的操作来改变用户对系统当中其它进程的可视度。如果想更深入的了解其内部机制,没有什么比用户手册更适合不过的了。
“User Namespace 对安全相关的标示、属性进行隔离,特别是User Id 和Group ID...”。
“在一个user namespace当中的进程所属的User以及Group可以与其有所不同。特别是,进程在user namespace外可以有普通的非特权user 同时在user 命名空间中有特权用户0,也就是在内部可以有所有的操作权限,但是在外部只能进行非特权相关的操作”。

下表提供了我们通过user namespace 想要实现的隔离

username space 说明

图片来源 https://endocode.com/blog/2016/01/22/linux-containers-and-user-namespaces/

再来说明下我们想要达成的目标, 将host 上的超级用户(其user id 是 UID = 0)与容器当中的root 用户进行隔离。

通过以上变更后,我们会得到神奇的效果,即使容器当中的应用 以root 用户去运行 并且其UID 是0,但是在现实当中并不会与host的超级用户有任何关联,也不会对系统造成影响。

你可能会觉得这无足轻重。但是,可以想象以下这样的场景,如果容器的的权限被贡献了,因为使用了这一技术,攻击者无法在跳出了容器后进行权限的升级以控制宿主机上的其它服务, 也就无法控制宿主机。


对docker 守护进程进行修改

首要的条件是启用Docker 守护进程的 userns-remap

值得指出的是,在现阶段你需要使用K8S V1.5 以上的版本以免由于使用了user namespaces 会对network namespace 进行破坏。从我观察到的是,K8S本身并不会产生问题,但是这是Network Namespace 的问题。

另外,由于docker 版本的变化,对docker daemon添加参数有可能有所变化。如果你所使用的版本滞后1个月以上的化,那么十分的不幸。对于持续的更新是有代价的;向后兼容性或者说会耗费大量的精力。对于我来说,这并不是问题毕竟技术很fantastic。

开始实验

第一步是使docker 守护进程通过json 配置文件来启动, 而不是通过unix 类型的配置。
添加 DOCKER_OPTS 到文件 /etc/default/docker 中。通过这种方式比通过修改 systemd 的unit 文件会简单的多。

在所提到的文件当中,添加下面的配置,

DOCKER_OPTS="--config-file=/etc/docker/daemon.json"

没错 /etc/docker/daemon.json 文件中包含是json类型的配置文件,简单起见,省略了其它的配置信息,添加了--userns-remap 选项

{

"userns-remap": "default"

}

对于老得版本或者说是不同的linux 发新版 或者是个人的偏好,也可以直接添加这个参数DOCKER_OPTS="--user-remap=default"至文件/etc/default/docker 当中而不使用json 配置文件

通过以下方式启动docker 守护进程

$ dockerd --userns-remap=default

希望通过以上的步骤对你来说是生效, 如果以上的配置对你来说不起作用还是Google吧


到这一阶段,会注意到在上面的例子当中为了方便起见default 作为重新映射的用户。稍后我们会接着讨论。现在,你可以跳转到其它必须的配置参数来启用user namespace。

如果你坚持像我上面所用的default 作为参数的化, 你应该添加以下的配置到下面的文件当中。对于Reahat 相关的发行版, 在重启按以上的配置后的docker daemon以前修改下列文件。对于其它的发行版,此文件有可能不存在,通过 echo 或者其它的命令创建此文件。

echo "dockremap:123000:65536" >> /etc/subuid

echo "dockremap:123000:65536" >> /etc/subgid

按照不同的发行版执行相关的命令 重启docker daemon,如

$ systemctl restart docker

  • 注意: 此种启动方式只适用于修改了daemon.json文件

核心内容

通过在上面的文件当中添加subordinate dockerremap user 和group 配置, 意思是说需要将容器的user ID和Group ID映射到宿主机的从123000到65536当中的一个值。理论上来说可以用65536 作为以上的起始值,但是实际来看这有所不同。在目前的版本当中, Docker 仅对第一个,UID进行映射。Docker 官方生成会在以后的版本中进行改进。

上面说过会对所使用的default 的配置进行说明。这个值是是docker 内部可以像我们所看到的那样使用用户名和组名dockerremap。可以使用任意的名,但是要在重启docker daemon 之前对文件 /etc/subuid/etc/subgid进行正确的修改。

其它的更改

注意,需要重新的拉取docker 镜像文件 之后会在宿主机的新的子目录进行更新。

如果查看 /var/lib/docker 目录后,你会发现镜像存储目录由我们所熟悉的UID.GID 数字格式命名。

$ ls /var/lib/docker

drwx------. 9 dockremap dockremap 4096 Nov 11 11:11 123000.123000/

从现在开始,如果像以下方式这样进入容器内部,你会发现所运行的应用程序是以root 和UID 为0 的方式运行。
运行容器

ubuntu@ubuntu-xenial:~$ sudo docker run --rm -d redis
sudo: unable to resolve host ubuntu-xenial
Unable to find image 'redis:latest' locally
latest: Pulling from library/redis
065132d9f705: Downloading 5.291MB/30.11MB
be9835c27852: Download complete
065132d9f705: Downloading 17.43MB/30.11MB
ea1f878b621a: Downloading 5.799MB/8.268MB
065132d9f705: Pull complete
be9835c27852: Pull complete
f4a0d1212c38: Pull complete
ea1f878b621a: Pull complete
7a838393b4b9: Pull complete
9f48e489da12: Pull complete
Digest: sha256:8a54dcc711406447b3631a81ef929f500e6836b43e7d61005fa27057882159da
Status: Downloaded newer image for redis:latest
4eb610618d04254245bbe29f3523641f2c4c1c9731e4c139f9e68a7e1c47de16

$ docker exec -it 4eb610618d04 bash

在物理机上,可以通过ps 命令来查看尽管该容器是以UID 为的0的用户来运行,但是实际上在物理机上,其值是UID=123000。

$ ps -ef | grep redis

如果系统支持的话,可以通过在宿主机和容器当中运行以下命令来获得相应的UID相关信息。

$ ps -eo uid,gid,args

限制条件

和所有非原生的安全方案一样,也有一些取舍,然而对于User namespace来说不那么的繁重。

注意,通过以上的配置后,在启动容器时就无法使用--net=host 或者使用--pid=host来共享 PID 。而且,你不能使用--read-only 的容器,因为对于一个使用了User Namespace的无状态的容器来说没有什么作用。

此外,--privileged 模式的参数也无法使用,并且需要确保任何所挂载的文件系统,如NFS,允许UIDsGIDs访问

对于像RedHat及其衍生的发行版,如Centos, 需要在boot loader中开启kernel的配置来启用User Namespace,通过grubby 命令进行配置:

$ grubby --args="user_namespace.enable=1" --update-kernel="$(grubby --default-kernel)"

重启机器后使参数生效。

通过以下配置禁用该模式:

$ grubby --remove-args="user_namespace.enable=1"
--update-kernel="$(grubby --default-kernel)";

结束语

希望以上简单的配置能够对Docker 主机的安全防护有所帮助。

最后一点, 任何人都不想获取了超级用户访问权限攻击者潜伏在主机当中,经过数月的学习研究配置的漏洞,即APT(Advanced Persistent Threat)的情况发生. 当然,任何人都不想要这一结果, 但是这却在你毫不知情的情况下实实在在的发生。仅仅是因为你从Docker hub中拉取了一个没有更新有漏洞包的镜像。保持警惕!

On that ever so cheery note: Stay vigilant!


实验playback

环境信息

ubuntu@ubuntu-xenial:~$ uname -r
4.4.0-96-generic
ubuntu@ubuntu-xenial:~$ cat /etc/issue.net
Ubuntu 16.04.3 LTS

ubuntu@ubuntu-xenial:~$ docker --version
Docker version 17.06.2-ce, build cec0b72

参考

Introduction to User Namespaces in Docker Engine
Control and configure Docker with systemd
Its all about linux