未来 Docker 的安全

g2b4 10年前

我在Opensource.com上写这些有关Docker安全的东西,想阐述的就一点——“纸已经包不住火了(containers do not contain)”

Red Hat和Docker其中一个主要的目标,就是确保这一陈述不是绝对正确的。我在Red Hat的小组仍在努力利用别的安全机制使container更加安全。这些少数的几个安全特色,我们目前正在攻克,他们未来可能成为影响Docker和container的方式。

用户namespace

用户namespace基于内核namespace,可以更好地将主机和容器分离起来。

基本方法就是宿主机创建一系列的UID,例如60000-61000,然后映射到内部的namespace 0-1000,也可以用GID实现。内核会把docker容器内的UID 0 识别、鉴定为外部的UID 60000,任何一个没有被映射过的进程或者UID在容器内部都会被识别为UID=-1,从而禁止他们访问容器,包括所有的镜像都不允许访问。如果你想使用有用户namespace认证的镜像,那么你就得切换到容器内部root用户的UID对应的外部ID。另一个问题就是外部UID 0挂载进入容器的卷、硬盘等设备在容器内部是没有权限访问的。你必须用`chown`命令将UID切换成内部可以的UID。

chown -R 60000:60000 /var/lib.content

用户namespace的另一个问题在于,你不许给不同的容器分配不同段的UID进行映射。如果你有成百上千个容器,那么如何宿主机分配映射的UID就是个问题。

用户namespace是一个很炫酷的事情,他们可以直接利用namespace的优势,如果把容器进行如上的操作,那么我们可以抛弃掉所有的宿主系统提供的功能。也就是说,当用户namespace生效后,我们可以完全不再需要宿主系统提供的功能。我们也不再需要selinux提供的安全标签的策略了。

使用案例

三个可以用到用户namespace的方案如下:

1.只映射UID=0。增加容器之间的隔离度,甚至可以完全抛弃capabilities机制。这样做无疑可以增强容器和宿主机之间的安全性,但是也不会相对地提升容器之间的隔离度。所以只需要假设所有DOCKER容器的ROOT的外部UID为2,那么,外部的UID=2映射到容器内部UID=0,外部GID=2映射到内部GID=0,凡是大于2的全部映射到自己。这样能最大程度的减少从容器内部获取宿主机的root权限。当然这样也能减少挂载文件系统时遇到的麻烦。这样处理之后,内部root用户mount的磁盘,外部是无法读取,(也就不会有SUID这个问题)。同样,其余的关于用户的namespace的处理也是一样。

2.openshift式的解决办法。每个容器都有自己单独的UID/GID,如同每个用户都有自己的UID和GID一样。只有容器用得到内核capabilities时这样才有必要,用不到的时候意义不大。

3.按照段来映射。每个容器都映射一个UID段,每个容器映射的UID段各不相同。但是复杂性会随着容器数目增加急剧增加。挂载文件系统可能会成为一个大问题。可以增加一个功能,当容器内部mount的时候便执行对应的chown UID:GID /SRC命令,这样一定程度上可以解决挂载的问题。

然而我不认为这三个解决方案可以叠加。我也看过对内核“加入容器时,重设mount目录的UID的提议”,甚至对乐死mount --bind的命令也执行重设UID,但是我觉得这个提议交给那些写内核的人比较好,我也会继续参考那些做安全人的意见。

用户namespace已经并入libcontainer了,也已经打好了能这样让docker运行起来的补丁。

Seccomp

有个问题,那就是这里和别的地方提到的容器隔离措施全都是依赖与内河。空气和电脑接触,但是空气无法和电脑内核交流。但是容器就不一样,所有的容器都是直接和内核进行交换信息。如果宿主机有个漏洞,那么这个漏洞就击垮所有的安全措施,docker容器也会变得无法控制。

X86_64的linux内核有超过600个系统调用。只要其中其中有一个有漏洞,那么都可以导致容器的权限提升。因此有些系统调用是基本用不到的,所以应该禁止使用这些系统调用。禁止的越多越安全。

Seccomp是google开发的禁止系统调用的沙盒类型的程序。例如Chrome的插件就得管管,毕竟插件都是来自互联网,没办法保证100%的安全。Google在Chrome浏览器中使用Seccomp执行chrome插件确保系统的安全。

我的同事Paul Moore,决定将Seccomp简化从而构建一个C库来管理系统调用库。现在他的成果Libseccomp也在qemu,lxc,和systemd等软件中开始使用了。

我们也开始用go封装这个库然后再libcontainer中调用,进行过滤系统调用。

一般而言我们认为这些系统调用是可以禁止容器调用的:kexec_load, open_by_handle_at, init_module, finit_module, delete_module, iopl, ioperm, swapon, swapoff, sysfs, sysctl, adjtimex, clock_adjtime, lookup_dcookie, perf_event_open, fanotify_init, kcmp.

我们也在寻求其他可以禁止的系统调用,欢迎给提议。除此之外,我们考虑禁止调用许多旧的网络调用: Amateur Radio X.25 (3), IPX (4), Appletalk (5), Netrom (6), Bridge (7), ATM VPC (8), X.25 (9), Amateur Radio X.25 PLP (11), DECNet (12), NetBEUI (13), Security (14), PF_KEY key management API (15), 还有所有比AF_NETLINK (16)需求的权限更多的系统调用。

加入系统调用过滤器的另一个方面就是可以过滤掉所有非本架构的调用,例如X86-64的电脑默认情况下是无法使用32位的系统接口的。我们希望这个方案被设置成为seccomp的默认选项。

禁止了其他架构的系统调用,我们可以简单地认为我们直接缩小了一半被内核提权、内核攻击的风险。

调整seccomp

类似于系统capabilities和selinux标签,我们允许使用命令行自己决定自己需要使用/禁止那些系统调用:

`docker run -d --security-opt seccomp:allow:clock_adjtime ntpd`
这条命令将会允许容器内使用clock_adjtime调用,因为是ntpd服务,所以必须得用来调整时间。
同样,

`docker run -d --security-opt seccomp:deny:getcwd /bin/sh`

这条命令将会禁止容器内执行的shell查询当前自己所在的目录。redhat的Matt Heon有一个展示这个功能的短片。

视频地址:https://www.油Tube.com/watch?feature=player_embedded&v=sw3NjVMMXz8 

我们默认会禁止很多的系统调用,但是仍旧有很多很危险的系统调用没有被禁止。你可以全部禁止,然后慢慢地加入希望使用的系统调用。

`docker run -d --security-opt seccomp:deny:all --security-opt seccomp:allow:getcwd /bin/sh`
事实上,docker中运行sh需要比getcwd更多的系统调用。被禁止掉的调用都会记录在`/var/log/autit/audit.log`。如果audit没有运行的话,那么将会记录在`/var/log/messages`中。

未来的docker

我们将会继续增强docker的安全功能。如果linux内核中出现新的安全功能,或者linux内核中有安全功能的改进,我们也会去尽量的利用这些功能,加固容器的安全性。

另一个方面是我们开始关注容器的管理。目前的管理方式是,如果你能有权限对docker的socket和端口发送数据的话,那么你就能对docker做任何事情。(译注:尤其是端口,docker remote api这种东西目前完全是无法禁止的) 很遗憾的是目前我们的解决办法只有禁止非root用户的/run/docker.socket接口的访问。我们也开始着手增加授权,这样管理员就能证明自己是管理员,而不是有权力访问接口的别的用户。我们也开始增加适当的管理记录的功能,将管理员对某些容器的是否是特权运行的情况记录到syslog或者journalctl。除此之外,我们还可能增加基于角色的访问控制(RBAC),简单的来说,就是超级管理员控制其他的管理员。例如:

  • 一星级管理员只允许开启、停止容器。

  • 二星级管理员有权利新建非特权的容器。

  • 三星级管理员有最大的权力运行所有的容器,并且赋予容器最大的权限。

结论

当这些安全措施实施之后,docker容器会更大程度的让你的宿主机远离风险。我们的目标就是让纸也能包下火the ability for containers to contain,呼应译文第一段)。