Docker镜像并不安全

  categories:资料  author:

docker镜像大部分是从网络上下载的, 安全性是不太靠谱的, 下面是一篇翻译过来的文章的内容, 后面是从网络上搜索到的, 一些安全建议等。

最近使用Docker下载“官方”容器镜像的时候,我发现这样一句话:

  1. ubuntu:14.04: The image you are pulling has been verified (您所拉取的镜像已经经过验证)

起初我以为这条信息引自Docker大力推广的镜像签名系统,因此也就没有继续跟进。后来,研究加密摘要系统的时候——Docker用这套系统来对镜像进行安全加固——我才有机会更深入的发现,逻辑上整个与镜像安全相关的部分具有一系列系统性问题。

Docker所报告的,一个已下载的镜像经过“验证”,它基于的仅仅是一个标记清单(signed manifest),而Docker却从未据此清单对镜像的校验和进行验证。一名攻击者以此可以提供任意所谓具有标记清单的镜像。一系列严重漏洞的大门就此敞开。

镜像经由HTTPS服务器下载后,通过一个未加密的管道流进入Docker守护进程:

  1. [decompress] -> [tarsum] -> [unpack]

这条管道的性能没有问题,但是却完全没有经过加密。不可信的输入在签名验证之前是不应当进入管道的。不幸的是,Docker在上面处理镜像的三个步骤中,都没有对校验和进行验证。

然而,不论Docker如何声明,实际上镜像的校验和(Checksum)从未经过校验。下面是Docker与镜像校验和的验证相关的代码片段,即使我提交了校验和不匹配的镜像,都无法触发警告信息。

  1. if img.Checksum != “” && img.Checksum != checksum {
  2. log.Warnf(“image layer checksum mismatch: computed %q,
  3. expected %q”, checksum, img.Checksum)
  4. }

不安全的处理管道

解压缩

Docker支持三种压缩算法:gzip、bzip2和xz。前两种使用Go的标准库实现,是内存安全(memory-safe)的,因此这里我预计的攻击类型应该是拒绝服务类的攻击,包括CPU和内存使用上的当机或过载等等。

第三种压缩算法,xz,比较有意思。因为没有现成的Go实现,Docker 通过执行(exec)xz二进制命令来实现解压缩。

xz二进制程序来自于XZ Utils项目,由大概2万行C代码生成而来。而C语言不是一门内存安全的语言。这意味着C程序的恶意输入,在这里也就是Docker镜像的XZ Utils解包程序,潜在地存在可能会执行任意代码的风险。

Docker以root权限运行 xz 命令,更加恶化了这一潜在威胁。这意味着如果在xz中出现了一个漏洞,对docker pull命令的调用就会导致用户整个系统的完全沦陷。

Tarsum

对tarsum的使用,其出发点是好的,但却是最大的败笔。为了得到任意一个加密tar文件的准确校验和,Docker先对tar文件进行解密,然后求出特定部分的哈希值,同时排除剩余的部分,而这些步骤的顺序都是固定的。

由于其生成校验和的步骤固定,它解码不可信数据的过程就有可能被设计成攻破tarsum的代码。这里潜在的攻击既包括拒绝服务攻击,还有逻辑上的漏洞攻击,可能导致文件被感染、忽略、进程被篡改、植入等等,这一切攻击的同时,校验和可能都是不变的。

解包

解包的过程包括tar解码和生成硬盘上的文件。这一过程尤其危险,因为在解包写入硬盘的过程中有另外三个已报告的漏洞。

任何情形下未经验证的数据都不应当解包后直接写入硬盘。

libtrust

Docker的工具包libtrust,号称“通过一个分布式的信任图表进行认证和访问控制”。很不幸,对此官方没有任何具体的说明,看起来它好像是实现了一些javascript对象标记和加密规格以及其他一些未说明的算法。

使用libtrust下载一个清单经过签名和认证的镜像,就可以触发下面这条不准确的信息(说不准确,是因为事实上它验证的只是清单,并非真正的镜像):

  1. ubuntu:14.04: The image you are pulling has been verified(您所拉取的镜像已经经过验证)

目前只有Docker公司“官方”发布的镜像清单使用了这套签名系统,但是上次我参加Docker管理咨询委员会的会议讨论时,我所理解的是,Docker公司正计划在未来扩大部署这套系统。他们的目标是以Docker公司为中心,控制一个认证授权机构,对镜像进行签名和(或)客户认证。

我试图从Docker的代码中找到签名秘钥,但是没找到。好像它并不像我们所期望的把密钥嵌在二进制代码中,而是在每次镜像下载前,由Docker守护进程通过HTTPS从CDN远程获取。这是一个多么糟糕的方案,有无数种攻击手段可能会将可信密钥替换成恶意密钥。这些攻击包括但不限于:CDN供应商出问题、CDN初始密钥出现问题、客户端下载时的中间人攻击等等。

补救

研究结束前,我报告了一些在tarsum系统中发现的问题,但是截至目前我报告的这些问题仍然没有修复。

要改进Docker镜像下载系统的安全问题,我认为应当有以下措施:

摒弃tarsum并且真正对镜像本身进行验证

出于安全原因tarsum应当被摒弃,同时,镜像在完整下载后、其他步骤开始前,就对镜像的加密签名进行验证。

添加权限隔离

镜像的处理过程中涉及到解压缩或解包的步骤必须在隔离的进程(容器?)中进行,即只给予其操作所需的最小权限。任何场景下都不应当使用root运行xz这样的解压缩工具。

替换 libtrust

应当用更新框架(The Update Framework)替换掉libtrust,这是专门设计用来解决软件二进制签名此类实际问题的。其威胁模型非常全方位,能够解决libtrust中未曾考虑到的诸多问题,目前已经有了完整的说明文档。除了已有的Python实现,我已经开始着手用Go语言实现的工作,也欢迎大家的贡献。

作为将更新框架加入Docker的一部分,还应当加入一个本地密钥存储池,将root密钥与registry的地址进行映射,这样用户就可以拥有他们自己的签名密钥,而不必使用Docker公司的了。

我注意到使用非Docker公司官方的第三方仓库往往会是一种非常糟糕的用户体验。Docker也会将第三方的仓库内容降为二等地位来看待,即使不因为技术上的原因。这个问题不仅仅是生态问题,还是一个终端用户的安全问题。针对第三方仓库的全方位、去中心化的安全模型既必须又迫切。我希望Docker公司在重新设计他们的安全模型和镜像认证系统时能采纳这一点。

结论

Docker用户应当意识到负责下载镜像的代码是非常不安全的。用户们应当只下载那些出处没有问题的镜像。目前,这里的“没有问题”并包括Docker公司的“可信(trusted)”镜像,例如官方的Ubuntu和其他基础镜像。

最好的选择就是在本地屏蔽 index.docker.io,然后使用docker load命令在导入Docker之前手动下载镜像并对其进行验证。Red Hat的安全博客有一篇很好的文章,大家可以看看。

感谢Lewis Marshall指出tarsum从未真正验证。

 

——————————–

安全建议:

Docker安全部署的17条建议

【编者的话】本文作者从Docker镜像、网络命名空间、日志和审核、守护进程特权、SELinux、二进制SUID/GUID、设备控制组、服务和应用、 Linux内核、用户命名空间、libseccomp等方面给出了自己的建议,可以一读。

当前持续增长的云计算市场对虚拟化技术有着强烈的需求。遗憾的是,大多数的虚拟化解决方案不够灵活,无法满足研发需求,且使用全虚拟化解决方案的潜在开销变成了制约基础设施扩展性的负担。

Docker让开发和运维人员能无缝地部署容器,用于运行业务所需的应用与服务,从而减少这类开销。然而,因为Docker与宿主系统使用同一内核,配置不当的容器将造成重大安全隐患。

以下列表每一条在相关容器环境内对提高其安全性提出了建议。需要注意的是,这些方案仅适用于部署在Linux主机上的Docker容器,并使用最新的Docker版本(1.4.0,commit 4595d4f,日期 11/12/14)。

以下部分内容参考了Jérôme Petazzoni[1]和Daniel J Walsh[2]的文章。本文旨在对他们的建议进行补充,并说明如何在Docker具体实现。

注:大多数建议的命令行选项可以在Dockerfile中以类似的方式保存和使用,实现自动化镜像构建。(译者注:原文17项建议以表格形式呈现,由于编辑器原因,这里将改为列表形式表述)

1. Docker镜像

Docker 1.3开始支持使用数字签名[3]来验证官方仓库镜像的来源和完整性。该功能仍在开发中,因此Docker只对(译者注:没有数字签名的)镜像发出警告信息但不会阻止其实际运行。此外,这点对非官方镜像不适用。

一般情况下,我们要确保只从受信任的库中获取镜像,并且不要使用--insecure-registry=[]参数。

2. 网络命名空间[4]

默认情况下,Docker守护进程暴露出来用于控制容器的REST API只能在本地通过Unix Domain Socket进行访问。

在一个TCP端口上运行Docker(比如,启动Docker守护进程时使用-H选项强制绑定地址)将允许任何可以访问该端口的人获取容器的访问权限,甚至在本地用户属于Docker组的某些情况下有可能获得宿主的root权限。[5]

在允许通过TCP访问守护进程时,确保通讯使用SSL加密[6]和权限控制能有效地防止未授权用户与其进行交互。

可在Docker的标准网络桥接接口docker0上启用内核防火墙iptables规则,用于加强这些控制。

例如,可以使用以下iptables过滤器[7]限制Docker容器的源IP地址范围与外界通讯。iptables -t filter -A FORWARD -s <source_ip_range> -j REJECT –reject-with icmp-admin-prohibited

3. 日志和审核

收集并归档与Docker相关的安全日志来达到审核和监控的目的。可以在宿主[8]上使用以下命令在容器外部访问日志文件:

docker run -v /dev/log:/dev/log <container_name> /bin/sh

使用Docker内置命令:

docker logs ... (-f to follow log output)

日志文件也可以导出成一个压缩包实现持久存储:

docker export ...

4. SELinux 或 AppArmor

通过访问控制的安全策略,可以配置Linux内核安全模块,如安全增强型Linux(SELinux)和AppArmor,从而实现强制性的访问控制(MAC)用以将进程约束在一套有限的系统资源或权限中。

如果先前已经安装并配置过SELinux,那么可以在容器使用setenforce 1来启用它。Docker守护进程的SELinux功能默认是禁用的,需要使用--selinux-enabled来启用。

容器的标签限制可使用新增的—-security-opt加载SELinux或者AppArmor的策略进行配置,该功能在Docker版本1.3[9]引入。

例如:

docker run --security-opt=secdriver:name:value -i -t centos bash

5. 守护进程特权

不要使用--privileged命令行选项。否则将允许容器访问宿主上的所有设备,另外,为容器提供特定的LSM(例如SELinux或AppArmor)配置,将给予它与运行在宿主上的进程同等访问权限。

避免使用--privileged有助于减少攻击面和可能的宿主威胁。但是,这并不意味着运行守护进程时不需要root权限,在最新版本中这仍然是必须的。

启动守护进程和容器的权限只能赋予受信任的用户。

可通过使用-u选项弱化容器内访问权限。例如:

docker run -u <username> -it <container_name> /bin/bash

Docker组的任何用户部分可能最终从容器中的主机上获得根源。

6. cgroups[10]

为了防止通过耗尽系统资源引发拒绝服务(DoS)攻击,可使用特定的命令行参数被来启用一些资源限制。

CPU使用:

docker run -it --rm --cpuset=0,1 -c 2 ...

内存使用:

docker run -it --rm -m 128m ...

存储使用:

docker -d --storage-opt dm.basesize=5G 

磁盘I/O:
目前Docker不支持。通过systemd暴露的BlockIO*特性可以在支持的操作系统中用来控制磁盘使用配额。

7. 二进制SUID/GUID

SUID和GUID程序在受攻击导致任意代码执行(如缓冲区溢出)时将非常危险,因为它们将运行在进程文件所有者或组的上下文中。

如果可能的话,使用特定的命令行参数减少赋予容器的能力,阻止SUID和SGID生效。

docker run -it --rm --cap-drop SETUID --cap-drop SETGID ...

还有种做法,可以考虑在挂载文件系统时使用nosuid属性来移除掉SUID能力。

最后一种做法是,删除系统中不需要的SUID和GUID程序。这类程序可在Linux系统中运行以下命令而找到:

find / -perm -4000 -exec ls -l {} \; 2>/dev/null
find / -perm -2000 -exec ls -l {} \; 2>/dev/null

然后,可以使用类似于下面的[11]命令将移除SUID和GUID文件权限:

sudo chmod u-s filename   sudo chmod -R g-s directory

8. 设备控制组(/dev/*)

如果需要,使用内置的--device选项(-v参数不要与–privileged一起使用)。此功能在1.2版本[12]引入。

例如(使用声卡):

docker run --device=/dev/snd:/dev/snd …

9. 服务和应用

如果Docker容器有可能被入侵,为了减少横向运动的可能,应考虑隔离敏感服务(如在宿主或虚拟机上运行SSH服务)。

此外,不要在容器内使用root权限运行不受信任的应用。

10. 挂载点

使用原生容器库(如libcontainer)时,Docker会自动处理这项。

但是,使用LXC容器库时,敏感的挂载点最好以只读权限手动挂载,包括:

/sys
/proc/sys
/proc/sysrq-trigger
/proc/irq
/proc/bus

挂载权限应在之后移除,以防止重新挂载。

11. Linux内核

使用系统提供的更新工具(如apt-get、yum等)确保内核是最新的。过时的内核相比已公开的漏洞危险性更大。

使用GRSEC或PAX来强化内核,例如针对内存破坏漏洞提供更高的安全性。

12. 用户命名空间

Docker不支持用户命名空间,但它是目前正在开发[13]的功能。现在,LXC驱动支持UID映射,但原生的libcontainer库不支持。

该功能允许Docker守护进程以非特权用户身份运行在宿主上,但在容器内看起来像是以root运行。

13. libseccomp(和seccomp-bpf 扩展)

libseccomp库允许基于白名单方法来限制Linux内核的系统调用程序的使用。最好禁用受攻击容器中对于系统操作不是很重要的系统调用程序,以防止其被滥用或误用。

此功能目前正在开发中(LXC驱动中存在,但是现在默认的libcontainer中没有)。使用LXC驱动程序[14]来重启Docker程序:

docker -d -e lxc

如何生成seccomp配置的说明都在GitHub仓库的“contrib”[15]文件夹。之后可用下面的命令来创建一个以LXC为基础的Docker容器:

docker run --lxc-conf="lxc.seccomp=$file" <rest of arguments>

14. 能力[7]

尽可能降低Linux能力。Docker默认的能力包括:chowndac_overridefownerkill、setgid、setuid、setpcap、net_bind_service、net_raw、sys_chroot、mknod、setfcap、和audit_write`。

在命令行启动容器时,可以通过--cap-add=[]--cap-drop=[]进行控制。

例如:

docker run --cap-drop setuid --cap-drop setgid -ti <container_name> /bin/sh

此功能在Docker 1.2版本引入[16]。

15. 多租户环境

由于Docker容器内核的共享性质,无法在多租户环境中安全地实现责任分离。建议将容器运行在没有其它目的,且不用于敏感操作的宿主上。可以考虑将所有服务迁移到Docker控制的容器城。

可能的话,设置守护进程使用--icc=false,并根据需要在docker run时指定-link,或通过—-export=port暴露容器的一个端口,而不需要在宿主上发布。

将相互信任的容器的组映射到不同机器上[17]。

16. 完全虚拟化

使用一个完全虚拟化解决方案来容纳Docker,如KVM。如果容器内的内核漏洞被发现,这将防止其从容器扩大到宿主上。

如同Docker-in-Docker工具[18]所示,Docker镜像可以嵌套来提供该KVM虚拟层。

17. 安全审核

定期对你的宿主系统和容器进行安全核查,以找出可能导致系统被入侵的错误配置或漏洞。



快乐成长 每天进步一点点