第10章 Docker与安全
本章主要内容
- Docker提供的开箱即用的安全方案到了何种水平
- Docker为更加安全做了哪些努力
- 其他开发商为了安全正在付出怎样的努力
- 为了改善安全问题,还可以采取哪些步骤
- 在多租户环境下如何管理用户
正如Docker在其文档中明确指出的,对于Docker API的调用需要root权限,这也就是为什么Docker通常需要用sudo命令来运行,或者必须把用户加入一个允许使用Docker API的用户组(该组可能叫作docker或者dockerroot)。
本章中我们将看一下Docker的安全问题。
10.1 Docker访问权限及其意味着什么
读者可能想知道一个用户如果可以运行Docker会造成多大的破坏。一个简单的例子是,这个命令(不要运行!)可能会删除宿主机/sbin目录下的所有二进制文件(如果拿掉了那个伪造的—donotrunme标志):
docker run —donotrunme -v /sbin:/sbin busybox rm -rf /sbin
值得指出的是,即使你不是root用户这段代码仍然生效。下面这条命令将会展示宿主机上安全shadow密码文件的内容:
docker run -v /etc/shadow:/etc/shadow busybox cat /etc/shadow
Docker的不安全性经常遭到误解,一部分是由于对内核中的命名空间的好处的误解。Linux命名空间提供了对系统中其他部分的隔离,但是对Docker的隔离级别是由用户自行决定的(正如前述的几个docker run例子所展示的)。而且,Linux操作系统不是所有部分都能够使用命名空间。设备和内核模块就是两个没有使用命名空间的Linux核心功能的例子。
Linux命名空间 Linux命名空间是设计来为进程提供独立于其他进程的系统视角。例如,进程命名空间意味着容器只能够看到与容器有关的进程——在同一宿主机上运行的其他进程对其而言是不可见的。网络空间命名意味着容器似乎有自己独有的网络栈可用。命名空间成为Linux内核的一部分已有多年。
而且,由于用户在容器内部可以通过系统调用来与内核进行root级别的交互,所以任何的内核缺陷都可能在Docker容器内被利用。当然,虚拟机通过连接到hypervisor可以达到同样的攻击级别,只是危害小一些。hypervisor本身也被爆出一些安全缺陷。
另一种理解的方式是认为运行容器与能通过包管理工具安装程序没什么区别(从安全的角度看)。
换句话说,用户对运行Docker容器的安全需求应该和安装软件包是一样的。如果用户有Docker,可以作为root用户安装软件。这也有人说Docker最好被理解为一个软件打包系统的部分原因。
用户命名空间 有些工作正在进行以期望通过用户命名空间来解除此风险,它可以把容器中的root用户映射到宿主机上的非特权用户。
你在乎吗
考虑到调用Docker API和root权限是等价的,接下来的问题就是:“你在乎吗?”尽管这话看上去有点儿奇怪,安全全然是关乎信任的,如果你信任用户在他们操作的环境里安装软件,那么他们在那儿运行Docker就应该没有障碍。安全的困难性主要体现在多租户环境下。由于容器内的root用户在关键的方面都和容器外的root用户一样,所以系统内拥有众多root用户可能会有隐患。
多租户 一个多租户环境就是许多用户共享同样的资源的环境。举例来说,两个团队可能通过两个不同的虚拟机来共享同一台服务器。多租户通过共享硬件而非向特定应用提供硬件来节省开支。但是它可能带来一些足以抵消开支节省的挑战,包括服务可靠性以及安全隔离。
一些组织采取了把每个用户的Docker运行在专用虚拟机上的做法。这台虚拟机可以用于安全隔离、运维隔离或者资源隔离。在虚拟机的信任界限内,用户运行Docker容器以获取其性能和运维上的好处。这就是Google计算引擎采取的方式,为了更进一步的安全性和其他一些运维上的好处,在用户的容器和其运行的基础设施间再加一个虚拟机。Google公司拥有大量可用的计算资源,所以他们不介意这样做的开销。
10.2 Docker中的安全手段
Docker维护者采取了多种手段来减少运行容器的安全风险。例如:
- 某些特定的核心挂载点(如/proc和/sys)现在是只读方式挂载的;
- 降低了默认的Linux能力;
- 现在已经存在对第三方安全系统(如SELinux和AppArmor)的支持。
本节中,我们会对这些有更深入的了解,可以采取其中的一些手段来降低在系统上运行容器的风险。
技巧84 限制能力
正如我们提到的,容器上的root用户和宿主机上的root用户是一样的。但是不是所有的root都生而平等。Linux允许为root用户分配进程级别的细粒度的权限。这些细粒度的权限被称为能力(capability),这样即使是root用户,也能限制他们所能做的破坏。本技巧展示了当运行Docker容器的时候如何操纵这些能力。
问题
想要降低容器在宿主机上进行破坏性活动的能力。
解决方案
使用—drop-cap标志来减少容器拥有的访问权限。
讨论
如果并非完全信任在系统上运行的容器的内容,可以通过减少容器可以获得的能力来降低出问题的风险。
1.Unix信任模型
为了了解本技巧的含义和作用,需要一点儿背景知识。当Unix系统被设计出来的时候,其信任模型并不复杂。存在被信任的管理员(root用户)和不被信任的用户。root用户可以做任何事情,然而普通用户只能影响他们自己的文件。因为这个操作系统一般是在大学实验室里使用而且本身不大,所以这个模型还是合理的。
随着Unix模型的发展以及互联网的到来,这个模型越来越不合理了。类似网络服务器的程序需要root权限来在80端口提供内容,同时也作为在宿主机上运行命令的有效代理。针对这些情况有些标准的应对模式,例如,绑定到端口80并把有效用户ID赋予一个非root用户。扮演着不同角色的用户,从系统管理员到数据库管理员,直到应用支持工程师和开发者,可能都需要对不同的系统上的资源有细粒度的访问权限。Unix 用户组从某种程度上减轻了这个问题,但是正如任何系统管理员都会说的那样,为这些权限需求建模并不是一个小问题。
2.Linux能力
为了尝试支持一个更加细粒度的对用户权限进行管理的方式,Linux内核工程师们开发了能力。它尝试把单块的root权限拆解成各个可以独立授予的功能片段。读者可以通过运行man 7 capabilities来查看更多细节(假设安装了帮助手册)。
有用的是,Docker默认关闭了一些特定的能力。也就是说,即使在容器内有root权限,有些事也做不了。例如,允许影响网络栈的CAPNET_ADMIN能力,默认就被禁用了。
表10-1列出了Linux的各项能力,给出了对它们允许做的事情的简要介绍,并且标明了它们是否在Docker容器内默认开启。记住,每项能力都和root用户改变其他用户在系统上的文件的能力有关。例如,容器内root用户仍然能chown宿主机上root的文件,只要该文件可以作为容器上的卷访问。
表10-1 Docker容器中的Linux能力
|
能 力 |
描 述 |
开启与否 |
|---|---|---|
|
|
对任意文件进行所有权改变 |
是 |
|
|
重载读、写和执行权限检查 |
是 |
|
|
当修改文件时,不清除suid和guid位 |
是 |
|
|
存储文件时,无视所有权检查 |
是 |
|
|
对于信号,绕过权限检查 |
是 |
|
|
使用 |
是 |
|
|
使用原始套接字和分组套接字,并且绑定到端口以进行透明代理 |
是 |
|
|
对进程的组所有权进行更改 |
是 |
|
|
对进程的用户所有权进行更改 |
是 |
|
|
设定文件能力 |
是 |
|
|
如果不支持文件能力,那么对来自其他进程和发往其他进程的能力进行限制 |
是 |
|
|
绑定套接字到小于1024的端口 |
是 |
|
|
使用 |
是 |
|
|
写入内核日志 |
是 |
|
|
启用/禁用内核日志记录 |
否 |
|
|
使用能阻止系统中止的特性 |
否 |
|
|
绕过读取文件和目录时的权限检查 |
否 |
|
|
锁定内存 |
否 |
|
|
绕过进程间通信对象权限 |
否 |
|
|
在一般文件上建立租约(对试图打开或者删除的监控) |
否 |
|
|
设立i-node标志 |
否 |
|
|
重载强制访问控制(和SmackLinux安全模组(SLM)有关) |
否 |
|
|
改变强制访问控制(和SLM有关) |
否 |
|
|
各种网络相关的操作,包括IP防火墙改变和网关配置 |
否 |
|
|
不再使用 |
否 |
|
|
一系列管理员功能。查看 |
否 |
|
|
重启 |
否 |
|
|
载载/卸载内核模块 |
否 |
|
|
操纵进程的nice优先级 |
否 |
|
|
开启或关闭进程记账 |
否 |
|
|
追踪进程的系统调用以及其他进程操纵能力 |
否 |
|
|
对系统很多核心部分进行输入/输出,如内存和SCSI设备命令 |
否 |
|
|
控制和重载多种资源限制 |
否 |
|
|
设置系统时钟 |
否 |
|
|
在虚拟终端上的特权操作 |
否 |
基于libcontainer引擎 如果读者不是在使用Docker默认的引擎容器(libcontainer),这些能力可能在读者安装的软件上有所不同。如果有系统管理员并且想要确认这些,就去请教他们。
但是,内核维护者仅在系统内分配了32个能力,所以这些能力都拓展了自己的范围,同时越来越多的细粒度root权限在内核外被创造出来。最值得一提的是,命名模糊的CAP_SYS_ADMIN能力涵盖了从改变宿主机域名到超出系统范围内打开文件数量的上限等多种不同行为。
一种极端的做法是从容器内移除所有在Docker默认开启的权限,然后看一下什么不工作了。在此我们运行一个移除所有默认开启的能力的bash脚本:
$ docker run -ti —cap-drop=CHOWN —cap-drop=DAC_OVERRIDE \—cap-drop=FSETID —cap-drop=FOWNER —cap-drop=KILL —cap-drop=MKNOD \—cap-drop=NET_RAW —cap-drop=SETGID —cap-drop=SETUID \—cap-drop=SETFCAP —cap-drop=SETPCAP —cap-drop=NET_BIND_SERVICE \—cap-drop=SYS_CHROOT —cap-drop=AUDIT_WRITE debian /bin/bash
如果通过这个脚本来运行程序,可以看到它是在什么地方如期失败,然后重新加上所需的能力。例如,用户可能需要改变文件所有权的能力,那么在前述的代码中就不能去掉FOWNER能力:
$ docker run -ti —cap-drop=CHOWN —cap-drop=DAC_OVERRIDE \—cap-drop=FSETID —cap-drop=KILL —cap-drop=MKNOD \—cap-drop=NET_RAW —cap-drop=SETGID —cap-drop=SETUID \—cap-drop=SETFCAP —cap-drop=SETPCAP —cap-drop=NET_BIND_SERVICE \—cap-drop=SYS_CHROOT —cap-drop=AUDIT_WRITE debian /bin/bash
移除/启用所有的能力 如果想要启用或者禁用所有的能力,可以使用
all而不是某个特定的能力,如docker run -ti —cap-drop=all unbuntu bash。
如果在bash脚本中运行了一些基本的命令,会发现这很有用。尽管在运行一些更复杂的程序的时候得到的好处可能会有所不同。
root还是root! 值得澄清的是,这些能力中的很大一部分是和影响其他用户的对象的root能力相关的,而不是root自己的对象。一个root用户仍然能够
chown宿主机上root的文件。例如,假设他们是在容器内操作并且通过卷加载的方式能够访问宿主机的文件。因此,纵使所有这些能力都关闭了,仍然值得把程序降级为一个非root用户。
这种对容器的能力的调优能力意味着对docker run使用—privileged标志就不必要了。需要能力的进程会被审计并且处于宿主机管理员的控制之下。
技巧85 Docker实例上的HTTP认证
在技巧1中我们看到了如何给守护进程打开网络访问权限,在技巧4中我们了解了如何使用socat来嗅探Docker API。
本技巧把二者结合起来:我们将能远程访问自己的守护进程并查看其反应。访问仅限于那些拥有用户名/密码组合的人,所以稍微安全些。除此之外还有好处,不用通过重启Docker守护进程来达到这个目的——启动一个容器守护进程吧!
问题
想要一些对Docker守护进程上可用的网络访问的基本验证。
解决方案
设立HTTP验证。
讨论
在本技巧中,我们将展示如何使用一种临时的办法把自己的Docker守护进程分享给其他人。图10-1给出了整体架构的布局。
图10-1 一个带有基本认证的Docker守护进程架构
假定的 Docker 默认设置 本讨论假设用户的Docker守护进程使用的是位于/var/run/docker.sock中的Docker默认的Unix套接字接入方法。
本技巧中的代码可以在https://github.com/docker-in-practice/docker-authenticate找到。代码清单10-1展示了这个仓库中的用来创建本技巧中的镜像的Dcokerfile。
代码清单10-1 用来创建dockerinpractice/docker-authenticate镜像的Dockerfile
FROM debianRUN apt-get update && apt-get install -y \nginx apache2-utils (1)RUN htpasswd -c /etc/nginx/.htpasswd username (2)RUN htpasswd -b /etc/nginx/.htpasswd username password (3)RUN sed -i ‘s/user .;/user root;/‘ \/etc/nginx/nginx.conf (4)ADD etc/nginx/sites-enabled/docker \/etc/nginx/sites-enabled/docker (5)CMD service nginx start && sleep infinity (6)
(1)确保需要的软件都安装并且更新好了
(2)❶为名为username的用户创建密码文件
(3)❷为名为username的用户创建密码为password
(4)nginx 需要以 root 权限运行以获取Docker Unix套接字,因此我们把用户那一行替换为了root用户的信息
(5)在Docker的nginx site文件中复制(代码清单10-2)
(6)默认开启nginx服务并且无限等待
在❶和❷中建立的密码文件包含在允许(或者禁止)连接到Docker套接字之前的凭证。如果用户在自己创建此镜像,可能想改变这两步中的username和password来定制对Docker套接字权限的凭证。
保持此镜像私有 小心不要共享此镜像,因为它包含你设置的密码!
Docker中的nginx site文件如代码清单10-2所示。
代码清单10-2 /etc/nginx/sites-enabled/docker
upstream docker {server unix:/var/run/docker.sock; (1)}server {listen 2375 default_server;(2)location / {proxy_pass http://docker; (3)auth_basic_user_file /etc/nginx/.htpasswd; (4)auth_basic “Access restricted”; (5)}}
(1)定义指向Docker的域套接字时nginx中“docker”的位置
(2)监听2375端口(标准的Docker端口)
(3)将发往和来自之前定义的“docker”地址的请求代理
(4)定义要用到的密码文件
(5)通过密码限制权限
现在把这个镜像作为守护进程容器运行起来,将需要的宿主机上的资源映射进来:
$ docker run -d —name docker-authenticate -p 2375:2375 \-v /var/run:/var/run dockerinpractice/docker-authenticate
这条命令会在后台以docker-authenticate为名运行,之后可以引用。在宿主机上暴露了容器的2375端口,而且这个容器加载了默认的含有Docker套接字的目录,从而能够访问Docker守护进程。如果读者用的是自己定制的镜像,有自己的用户名和密码,在这里需要把镜像名替换成自己的。
网络服务现在应该就启动运行起来了。如果读者使用自己设立的用户名和密码来curl这个服务,应该能看到一个API响应:
$ curl http://username:password@localhost:2375/info (1){“Containers”:115,”Debug”:0, (2)➥ “DockerRootDir”:”/var/lib/docker”,”Driver”:”aufs”,➥ “DriverStatus”:[[“Root Dir”,”/var/lib/docker/aufs”],➥ [“Backing Filesystem”,”extfs”],[“Dirs”,”1033”]],➥ “ExecutionDriver”:”native-0.2”,➥ “ID”:”QSCJ:NLPA:CRS7:WCOI:K23J:6Y2V:G35M:BF55:OA2W:MV3E:RG47:DG23”,➥ “IPv4Forwarding”:1,”Images”:792,➥ “IndexServerAddress”:”https://index.docker.io/v1/“,➥ “InitPath”:”/usr/bin/docker”,”InitSha1”:””,➥ “KernelVersion”:”3.13.0-45-generic”,➥ “Labels”:null,”MemTotal”:5939630080,”MemoryLimit”:1,➥ “NCPU”:4,”NEventsListener”:0,”NFd”:31,”NGoroutines”:30,➥ “Name”:”rothko”,”OperatingSystem”:”Ubuntu 14.04.2 LTS”,➥ “RegistryConfig”:{“IndexConfigs”:{“docker.io”:➥ {“Mirrors”:null,”Name”:”docker.io”,➥ “Official”:true,”Secure”:true}},➥ “InsecureRegistryCIDRs”:[“127.0.0.0/8”]},”SwapLimit”:0}
(1)在要curl的URL中写上username:password,地址放在@符号后。本请求是访问Docker守护进程API的/info端点
(2)从Docker守护进程返回的JSON
完成之后,通过下面的命令移除容器:
$ docker rm -f docker-authenticate
权限现在被收回了!
使用docker命令?
读者可能想知道其他用户能否通过docker命令来链接。例如,类似于:
docker -H tcp://username:password@localhost:2375 ps
在编写本书时,验证功能尚未被内置到Docker本身。但是我们已经创建了可以处理验证信息并且允许Docker连接到守护进程的镜像。简单地使用此镜像如下:
$ docker run -d —name docker-authenticate-client \ (1)-p 127.0.0.1:12375:12375 \ (2)dockerinpractice/docker-authenticate-client \ (3)192.168.1.74:2375 username:password (4)
(1)在该背景下运行客户端容器,并给它一个名字
(2)暴露出一个端口用来让Docker连接到守护进程,但是仅允许来自本地机器的连接
(3)使用我们创建的镜像以允许和Docker建立验证连接
(4)镜像的两个参数(指定验证连接的另一端以及用户名和密码)应当替换为你自己设立的合适的值
注意,对于指定验证连接的另一端来说,localhost或者127.0.0.1是不起作用的,如果想要在一台宿主机上试用,必须使用ip addr来指定机器的外部IP地址。
现在可以用下列命令来使用验证连接:
docker -H localhost:12375 ps
注意,由于一些实现上的限制,交互式的Docker命令(run、exec带上-i参数)不能使用。
不要指望这样就安全了! 这样提供了基本的验证,但是这并不能提供真正意义上的安全(尤其能够监听你网络通信的人可以拦截你的用户名和密码)。更加需要的是创建一个用TLS保护的服务器,接下来的技巧中会介绍。
技巧86 保护Docker API
在本技巧中我们会展示可以通过TCP端口向其他人开放自己的Docker服务器,同时确保只有受信任的用户才能连接。这是通过创建一个只有受信任的宿主机才能得到的密钥来实现的。只要受信任的密钥在服务器和客户端之间保持秘密,那么Docker服务器应当就是安全的。
问题
想要Docker API能通过端口安全地服务。
解决方案
创建一个自签名的证书并且带上—tls-verify标志来运行Docker守护进程。
讨论
本安全方法依赖于服务器上创建的所谓密钥文件(key file)。这些文件是通过一些特殊工具来创建的,确保了如果没有服务器密钥(server key),就很难复制。图10-2大体介绍了这种方法是如何工作的。
图10-2 创立密钥以及分发
什么是服务器密钥和客户端密钥 服务器密钥是一个保存有仅服务器知道的秘密数字的文件,读取被服务器的主人分发出去的密钥(所谓客户端密钥)加密过的信息,它是必不可少的。一旦这些密钥创立并分发完毕,就可以用它们来建立客户端和服务器之间的安全连接。
1.创建Docker服务器证书
首先要创建证书和密钥。产生密钥需要使用OpenSSL包。在终端运行openssl命令来检查它是否已经安装好了。如果没有安装,在使用下面的代码来产生证书和密钥之前需要进行安装:
$ sudo su (1)$ read -s PASSWORD (2)$ read SERVER$ mkdir -p /etc/docker (3)$ cd /etc/docker$ openssl genrsa -aes256 -passout pass:$PASSWORD \ (4)-out ca-key.pem 2048$ openssl req -new -x509 -days 365 -key ca-key.pem -passin pass:$PASSWORD \-sha256 -out ca.pem -subj “/C=NL/ST=./L=./O=./CN=$SERVER” (5)$ openssl genrsa -out server-key.pem 2048 (6)$ openssl req -subj “/CN=$SERVER” -new -key server-key.pem \-out server.csr (7)$ openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca-key.pem-passin “pass:$PASSWORD” -CAcreateserial \-out server-cert.pem (8)$ openssl genrsa -out key.pem 2048 (9)$ openssl req -subj ‘/CN=client’ -new -key key.pem\-out client.csr (10)$ sh -c ‘echo “extendedKeyUsage = clientAuth” > extfile.cnf’$ openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca-key.pem \-passin “pass:$PASSWORD” -CAcreateserial -out cert.pem \-extfile extfile.cnf (11)$ chmod 0400 ca-key.pem key.pem server-key.pem (12)$ chmod 0444 ca.pem server-cert.pem cert.pem (13)$ rm client.csr server.csr (14)
(1)确保你是root用户
(2)输入你证书的密码和你将用来连接Docker服务器的服务器名称
(3)如果docker配置目录不存在,创建它,并进入该目录
(4)使用2048位安全码产生证书授权(CA).pem文件
(5)用你的密码和地址给CA密钥签名一年期
(6)用2048位安全码产生服务器密钥
(7)用你宿主机的名字处理服务器密钥
(8)使用你的密码给密钥签名一年期
(9)用2048位安全码产生一个客户端密钥
(10)把这个密钥处理成客户端密钥
(11)用你的密码给密钥签名一年期
(12)把服务器文件的权限改为对root只读
(13)把客户端文件的权限改为对所有人只读
(14)删除剩余文件
辅助函数 一个叫CA.pl的脚本在你的系统里可能已经安装好了,它可以简化这个过程。我们在这里通过原始的
openssl命令的方式是因为这样更富有指导性。
2.设置Docker服务器
接下来需要在Docker守护进程文件里设置Docker选项来指定使用哪个密钥来为通信加密(参考附录B来了解如何配置以及重启Docker守护进程):
DOCKER_OPTS=”$DOCKER_OPTS —tlsverify” (1)DOCKER_OPTS=”$DOCKER_OPTS \—tlscacert=/etc/docker/ca.pem” (2)DOCKER_OPTS=”$DOCKER_OPTS \—tlscert=/etc/docker/server-cert.pem” (3)DOCKER_OPTS=”$DOCKER_OPTS \—tlskey=/etc/docker/server-key.pem” (4)DOCKER_OPTS=”$DOCKER_OPTS -H tcp://0.0.0.0:2376” (5)DOCKER_OPTS=”$DOCKER_OPTS \-H unix:///var/run/docker.sock” (6)
(1)告诉Docker守护进程你想要通过TLS加密的方式来保障连接的安全
(2)为Docker服务端指定CA文件
(3)为服务器指定证书
(4)指定服务器使用的私钥
(5)在2376端口把Docker守护进程通过TCP开放给外界
(6)按照一般的做法,通过一个套接字来在本地打开Docker守护进程
3.分发客户端密钥
接下来需要把密钥发送到客户端宿主机上以便其能连接到服务器并且交换信息。我们不希望向其他人展示自己的密钥,所以这个过程也需要安全地传送给客户端。一种相对安全的做法是通过SCP(安全复制)从服务器直接复制到客户端。SCP组件总体上是使用了我们在这里展示的技术来传输数据的,只是使用的是不同的密钥。
在客户端宿主机上,如同之前做的那样在/ect下创建Docker配置文件夹:
user@client:~$ sudo suroot@client:~$ mkdir -p /etc/docker
然后通过SCP把文件从服务器传输到客户端。确保在接下来的命令中把client替换为了你的客户端及其宿主机名。同时要保证对于要在客户端运行docker命令的用户来说,这些文件都是可读的。
user@server:~$ sudo suroot@server:~$ scp /etc/docker/ca.pem client:/etc/dockerroot@server:~$ scp /etc/docker/cert.pem client:/etc/dockerroot@server:~$ scp /etc/docker/key.pem client:/etc/docker
4.测试
为了测试你的设置,首先不带任何凭证向Docker服务器发起请求,应该被拒绝才对:
root@client~: docker -H myserver.localdomain:2376 infoFATA[0000] Get http://myserver.localdomain:2376/v1.17/info: malformed HTTP➥ response “\x15\x03\x01\x00\x02\x02”. Are you trying to connect to a➥ TLS-enabled daemon without TLS?
接下来使用凭证来链接,应该会返回一些有用的输出:
root@client~: docker —tlsverify —tlscacert=/etc/docker/ca.pem \—tlscert=/etc/docker/cert.pem —tlskey=/etc/docker/key.pem \-H myserver.localdomain:2376 info243 infoContainers: 3Images: 86Storage Driver: aufsRoot Dir: /var/lib/docker/aufsBacking Filesystem: extfsDirs: 92Execution Driver: native-0.2Kernel Version: 3.16.0-34-genericOperating System: Ubuntu 14.04.2 LTSCPUs: 4Total Memory: 11.44 GiBName: rothkoID: 4YQA:KK65:FXON:YVLT:BVVH:Y3KC:UATJ:I4GK:S3E2:UTA6:R43U:DX5TWARNING: No swap limit support
本技巧带来了两方面的好处——一个开放给其他人使用的Docker守护进程,以及只有受信任的用户可以访问。一定要保证密钥的安全!
10.3 来自Docker以外的安全
宿主机上的安全性不仅在于使用docker命令。在本节中读者会看到另外两种保障Docker容器安全的方式,这次是来自Docker以外的。
第一种方式展示了应用程序平台即服务(application platform as a service,aPaaS)的方式,它采用严加约束并受到管理员控制的方式运行Docker。作为例子,我们用Docker命令运行一个OpenShifit Origin服务器(一个以可控的方式部署Docker的aPaas)。我们会看到终端用户的能力会受到管理员的限制和管理,对于Docker运行时的访问也可以移除了。
第二种方式不止是这种程度的安全性,它使用SELinux(一个可以限制谁可以做什么的细粒度的安全技术)来进一步限制了运行中的容器内部的自由。
什么是SELinux SELinux是一个由美国国家安全局(NSA)创建并开源的工具,它满足了他们对于强访问控制的需求。至今为止它成为安全标准已经有段时间了,并且十分强大。但是,很多人遇到问题的时候都直接把它关了,而不是花时间来理解它。我们希望在这里展示的技巧能够让这种方式不那么让人望而却步。
技巧87 OpenShift——一个应用程序平台即服务
OpenShift是一个由Red Hat管理的产品,它允许一个组织运行应用程序平台即服务(aPaas)并为开发团队提供了一个不需要关心硬件细节就可以运行代码的平台。这个产品的第三版用Go语言进行了彻头彻尾的重写,使用Docker作为容器技术,并用Kubernetes和etcd进行编排。不仅如此,Red Hat还加入了一些企业级特性,使其能够简单地部署到企业及关注安全的环境中。
尽管我们可以讨论OpenShift的很多特性,但在这里我们只把它作为一种安全管理的方式,去除用户直接运行Docker的能力,同时又保持使用Docker的好处。
OpenShift 有企业支持的商业产品,也有开源项目名为Origin,后者在https://github.com/ openshift/origin维护。
问题
想要管理不受信任的用户调用docker run的安全风险。
解决方案
使用OpenShift这样的应用程序平台即服务(aPaaS)工具。
讨论
aPaaS有很多优点,我们在这里关注的是它管理用户权限和代表用户运行Docker容器的能力,这为运行Docker容器的用户提供了安全审计点。
为什么这一点很重要?使用这一aPaaS的用户没有调用docker命令的直接权限,因此,除非他们颠覆OpenShift提供的安全性,否则他们不能做出任何破坏。例如,默认来说容器是由非root用户部署的,想要克服这一点需要由管理员授权。如果不能信任用户,那么使用aPaaS是给他们Docker访问权限的高效方式。
什么是aPaaS aPaaS为用户提供了为开发、测试乃至生产环境按需快速启动应用程序的能力。Docker对这些服务天然适用,因为它提供了一种可靠而隔离的应用交付格式,让运维团队去处理部署细节。
简而言之,OpenShift在Kunbernets基础上构建(见技巧79),但是增加了一些合格的aPaaS的特性。这些额外的特性包括:
- 用户管理;
- 权限管理;
- 限额;
- 安全上下文;
- 路由。
1.安装OpenShift
对OpenShift的安装做一个完全介绍超出了本书的范围。
如果想要我们维护的使用Vagrant的自动安装,参见https://github.com/docker-in-practice/ shutit-openshift-origin。如果在安装Vagrant时需要帮助,参考附录C。
其他选项,如一个仅限Docker的安装(仅限单节点)或者完全手工的构建,在OpenShift Origin的代码库https://github.com/openshift/origin.git上都可以找到而且还有文档。
什么是OpenShift Origin OpenShift Origin是OpenShift的“上游”版本。“上游”这里是指它是由Red Hat同步了代码,为OpenShift定制了一些功能,它是OpenShift官方支持的作品。Origin是开源的,任何人都可以使用,也接受任何人的贡献。但它由Red Hat管理的版本是收费的,并且作为“OpenShift”项目受到支持。上游版本通常更加先进但是不那么稳定。
2.OpenShift应用程序
在本技巧中我们要使用OpenShift网络接口展示一个创建、构建、运行和访问应用程序的简单的例子。这个应用程序是一个提供简单Web页面的基本NodeJS应用程序。
这个应用程序会在底层用到Docker、Kubernetes和S2I。Docker用来封装构建和部署环境。来自技巧48的从源代码到镜像(S2I)的构建方法被用于构建 Docker 容器,Kubernetes被用于在OpenShift集群上运行应用程序。
3.登录
要开始登录,先在shutit-openshift-origin文件夹运行./run.sh,然后导航到https://localhost: 8443,忽视所有的安全警告。我们会看到图 10-3所示的登录页面。注意,如果是使用Vagrant安装的,那么需要在虚拟机里启动一个Web浏览器。(要了解如何给虚拟机添加图形用户界面,参见附录C。)
图10-3 OpenShift登录页面
使用任意密码以hal-1登录。
4.构建一个NodeJS应用
现在以开发者的身份登录到了OpenShift(参见图10-4)。
图10-4 OpenShift项目页面
点击Creat按钮来创建一个项目。如图10-5所示这样填写表单。然后再次点击Create按钮。
图10-5 OpenShift项目创建页面
一旦项目创建完成,再次点击Create按钮,输入推荐的 GitHub 仓库(https://github.com/ openshift/nodejs-ex),如图10-6所示。
图10-6 OpenShift项目源页面
点击Next按钮,要在众多创建镜像中选择一个,如图10-7所示。创建镜像定义了代码构建的上下文。选择NodeJS构建镜像。
图10-7 OpenShift构建镜像选择页面
现在像图10-8所示这样填写表单,滚动到表单下方,选择页面底部的Create on NodeJS。
图10-8 OpenShift NodeJS模板表单
几分钟后,屏幕应该如图10-9所示。
图10-9 OpenShift开始构建页面
过一会儿,如果向下滚动屏幕,就会看到构建已经开始,如图10-10所示。
图10-10 OpenShift构建信息窗口
构建没有开始? 在OpenShift早期版本中,构建有时不会自动开始。如果发生这种情况,几分钟后点击Start Build按钮即可。
过一会儿就能看到应用程序正在运行,如图10-11所示。
图10-11 程序运行页面
通过点击Browse和Pods,可以发现pod已经部署上去了,如图10-12所示。
图10-12 OpenShift pod清单
什么是pod 关于什么是pod的解释见技巧79。
如何访问pod?查看Services标签(如图10-13所示),可以看到用于访问的IP地址和端口号。
图10-13 OpenShift NodeJS应用程序服务细节
把浏览器指向这个地址,NodeJS应用程序就会运行起来,如图10-14所示。
图10-14 NodeJS应用程序登录页
5.总结
我们来总结一下到目前为止我们做到了什么以及它们对安全的重要性。
从用户的角度看,他们不用接触Dockerfile或者使用docker run命令,就登录了一个Web应用程序,并使用基于Docker的技术部署了一个应用程序。
OpenShift的管理员可以:
- 控制用户访问权限;
- 限制项目用到的资源;
- 集中供应资源;
- 确保代码默认不是以高权限运行的。
这比直接让用户运行docker run安全多了!
6.接下来该怎么办
读者如果想在这个应用程序的基础上继续构建,了解 aPaaS 是如何促进迭代的,可以 fork这个Git仓库,在fork的仓库里修改代码,然后创建一个新应用程序。我们已经完成了这些,参见https:// github.com/docker-in-practice/nodejs-ex。
要阅读更多关于OpenShift的资料,可以访问http://www.openshift.org。
技巧88 使用安全选项
在之前的技巧里读者已经了解到了,默认情况下用户有Docker容器的root权限,这个用户和宿主机的root用户是一样的。为了改善这一点,我们展示了如何减少用户作为root用户的能力,以便即使他们脱离了容器,内核仍然不会允许有些操作执行。
然而,用户可以做更多事情。通过使用Docker的安全选项标志,用户可以防止宿主机上的资源受到容器内执行的操作的影响。这样就限制了容器仅能影响宿主机授权了的资源。
问题
想要保护宿主机不受容器操作的危害。
解决方案
使用内核支持的强制访问控制工具。
讨论
我们要使用SELinux作为强制访问控制(mandatory access control,MAC)工具。SELinux在某种程度上是工业标准,尤其受到关注安全的组织的青睐。它最初由NSA开发,用来保护他们的系统,随后被开源。它在基于Red Hat的系统上被当作标准使用。
SELinux是一个很大的话题,我们无法在本书中详尽讨论。接下来展示如何编写并实施一个简单的策略,以便读者能感受一下SELinux的工作方式。如果需要的话,读者可以更进一步或者做一些实验。
什么是MAC工具 除用户熟悉的标准安全规则之外,Linux中的强制访问控制(MAC)工具还执行很多。简而言之,它不仅确保文件和进程执行了常规读-写-执行规则,还可在内核级别对进程施加更细粒度的规则。例如,一个MySQL进程可能只允许在特定文件夹(如/var/lib/mysql)下写文件。基于Debian的系统上对应的标准是AppArmor。
本技巧假设用户有一个启用了SELinux的宿主机。也就是说,用户必须首先安装SELinux(如果没有安装好的话)。如果用户在运行Fedora或者其他基于Red Hat的系统,很可能已经装好了。
运行sestatus命令来确定是否启用了SELinux:
# sestatusSELinux status: enabledSELinuxfs mount: /sys/fs/selinuxSELinux root directory: /etc/selinuxLoaded policy name: targetedCurrent mode: permissiveMode from config file: permissivePolicy MLS status: enabledPolicy deny_unknown status: allowedMax kernel policy version: 28
第一行输出会说明 SELinux 是否已经启用了。如果这个命令不可用,就说明宿主机上没有安装SELinux。
用户还需要一些相关的SELinux策略创建工具。例如,在一个能使用yum的机器上,需要运行yum -y install selinux- policy-devel。
1.Vagrant机器上的SELinux
如果没有SELinux又想要构建它的话,可以使用一个ShutIt脚本来在宿主机内构建提前安装好Docker和SELinuxd的虚拟机。图10-15大体介绍了它做了些什么。
图10-15 提供SELinux虚拟机的脚本
什么是ShutIt ShutIt是一个我们原创的通用的shell自动化工具,可以突破Dockerfile的一些局限。如果想要了解更多,参见GitHub网页http://ianmiell.github.io/shutit。
图10-15中列出了建立策略所需的步骤。脚本会做以下几件事:
(1)创建虚拟机;
(2)启动一个合适的Vagrant镜像;
(3)登录这台虚拟机;
(4)确保SELinux的状态正确;
(5)安装最新版的Docker;
(6)安装SELinux策略开发工具;
(7)给你一个shell。
下面是用来设置和运行的它的命令(在Debian和基于Red Hat的发行版上测试过):
sudo su -(1)apt-get install -y git python-pip docker.io || \yum install -y git python-pip docker.io (2)pip install shutit (3)git clone https://github.com/ianmiell/docker-selinux.git (4)cd docker-selinuxshutit build —delivery bash \ (5)-s io.dockerinpractice.docker_selinux.docker_selinux \compile_policy no (6)
(1)在开始运行前确保你是root用户
(2)确保在宿主机上安装了所需要的包
(3)安装ShutIt
(4)复制SELinux ShutIt脚本并进入其目录
(5)运行ShutIt脚本。—delivery bash意味着命令是在bash中执行的而非通过SSH或者在Docker容器中执行
(6)设置脚本不要去编译SELinux策略,以为我们会手动做这一步
运行这个脚本之后,最后应该可以看到下面这样的输出:
Pause point:Have a shell:You can now type in commands and alter the state of the target.Hit return to see the promptHit CTRL and ] at the same time to continue with buildHit CTRL and u to save the state
现在在虚拟机内有一个安装了SELinux的shell在运行了。如果输入sestatus,可以看到SELinux以宽容(permissive)模式开启(如代码清单10-3)。按Ctrl+]以返回宿主机的shell。
2.编译SELinux策略
不管使用ShutIt脚本与否,我们都假设读者有一台启用了SELinux的宿主机。输入sestatus来获得一个状态汇总(如代码清单10-3所示)。
代码清单10-3 SELinux状态总结
# sestatusSELinux status: enabledSELinuxfs mount: /sys/fs/selinuxSELinux root directory: /etc/selinuxLoaded policy name: targetedCurrent mode: permissiveMode from config file: permissivePolicy MLS status: enabledPolicy deny_unknown status: allowedMax kernel policy version: 28
我们处于宽容模式,也就说SELinux会把违反安全的行为记录在日志里,但是不会强制实施。这样就可以安全地测试新策略而不会搞得系统没法用。用root身份录入setenforce Permissive来把SELinux的状态改为宽容状态。如果因为安全原因没法在自己的宿主机上这么做,不用担心,在代码清单10-4中有一个把策略设为宽容的选项。
在守护进程上设置
—selinux-enabled如果在宿主机上自行安装SELinux和Docker,一定要确保Docker守护进程设置了—selinux-enabled标志。读者可以使用ps -ef | grep ‘docker -d.— selinux-enabled来检查,它应该会在输出中返回一些匹配结果。
给策略创建一个文件夹并进入这一文件夹,然后以root身份创建代码清单10-4所示的策略文件。这个策略文件包含我们将要采用的策略。
代码清单10-4 创建SELinux策略
mkdir -p /root/httpd_selinux_policy &&➥ cd /root/httpd_selinux_policy (1)cat > docker_apache.te << END (2)policy_module(docker_apache,1.0) (3)virt_sandbox_domain_template(docker_apache) (4)allow docker_apache_t self: capability { chown dac_override kill setgid➥ setuid net_bind_service sys_chroot sys_nice sys_tty_config} ; (5)allow docker_apache_t self:tcp_socket (6)➥ create_stream_socket_perms;allow docker_apache_t self:udp_socket➥ create_socket_perms;corenet_tcp_bind_all_nodes(docker_apache_t)corenet_tcp_bind_http_port(docker_apache_t)corenet_udp_bind_all_nodes(docker_apache_t)corenet_udp_bind_http_port(docker_apache_t)sysnet_dns_name_resolve(docker_apache_t) (7)#permissive docker_apache_t (8)END (9)
(1)创建一个保存策略文件的文件夹并且进入
(2)使用“原地”文档来创建要编译的策略文件
(3)使用policy_module指令创建SELinux策略模块docker_apache
(4)使用提供的模板来创建docker apache_t SELinux类型,它可以作为 Docker容器运行。这个模板给了docker_apache SELinu域运行起来的最小权限。我们会增加一些权限来让这个容器成为一个有用的环境
(5)Apache网络服务器要运行需要这些能力,所以用allow指令在这里添加这些能力
(6)这些allow和corenet规则给了容器在网络上监听Apache端口的权限
(7)使用sysnet指令允许DNS服务器解析
(8)或者设置docker_apache_t类型为宽容模式,以便即使宿主机在强制施行SELinux这个策略也不会强制施行。无法设置宿主机的SELinux模式的时候使用这个
(9)结束“原地”文档,将其写到磁盘
SELinux策略文档 为了获得关于前述授权的更多信息,了解其他授权,可以安装selinux-policy- doc包,然后用浏览器浏览位于file:///usr/share/doc/selinux-policy-doc/html/index.html的文档。这些文档在http://mcs.une.edu.au/doc/selinux-policy/html/templates.html上也提供。
现在编译策略,观察程序在强制模式下会启动失败。以宽容模式重启,检查违反情况并在之后改正:
$ make -f /usr/share/selinux/devel/Makefile \docker_apache.te (1)Compiling targeted docker_apache module/usr/bin/checkmodule: loading policy configuration from➥ tmp/docker_apache.tmp/usr/bin/checkmodule: policy configuration loaded/usr/bin/checkmodule: writing binary representation (version 17)➥ to tmp/docker_apache.modCreating targeted docker_apache.pp policy packagerm tmp/docker_apache.mod tmp/docker_apache.mod.fc$ semodule -i docker_apache.pp (2)$ setenforce Enforcing (3)$ docker run -ti —name selinuxdock➥ —security-opt label:type:docker_apache_t httpd (4)Unable to find image ‘httpd:latest’ locallylatest: Pulling from library/httpd2a341c7141bd: Pull complete[…]Status: Downloaded newer image for httpd:latestpermission deniedError response from daemon: Cannot start container➥ 650c446b20da6867e6e13bdd6ab53f3ba3c3c565abb56c4490b487b9e8868985:➥ [8] System error: permission denied$ docker rm -f selinuxdock (5)selinuxdock$ setenforce Permissive (6)$ docker run -d —name selinuxdock➥ —security-opt label:type:docker_apache_t httpd (7)
(1)把dokcer_apache.te文件编译为以.pp为后缀的二进制SELinux模块
(2)安装模块
(3)将SELinux模式设置为“强制”
(4)把httpd镜像作为守护进程运行,应用在模块里定义的docker_apache_t安全标签类型。这条命令会失败,因为它违反了SELinux安全配置
(5)移除刚创建的容器
(6)将SELinux模式设置为“宽容”,以允许程序启动
(7)把httpd镜像作为守护进程运行,应用在模块里定义的docker_apache_t安全标签类型。这条命令会成功运行
3.检查违反情况
到此为止,我们已经创建了一个SELinux模块并在宿主机上应用这一模块。因为在宿主机上SELinux的执行模式被设为了“宽容”,在“强制”模式里会被禁止的行为允许执行,同时会在审计日志里留下一条日志记录。可以通过运行以下命令来检查这些信息:
$ grep -w denied /var/log/audit/audit.logtype=AVC msg=audit(1433073250.049:392): avc: (1)➥ denied { transition} for (2)➥ pid=2379 comm=”docker” (3)➥ path=”/usr/local/bin/httpd-foreground” dev=”dm-1” ino=530204 (4)➥ scontext=system_u:system_r:init_t:s0➥ tcontext=system_u:system_r:docker_apache_t:s0:c740,c787 (5)➥ tclass=process (6)type=AVC msg=audit(1433073250.049:392): avc: denied { write} for➥ pid=2379 comm=”httpd-foregroun” path=”pipe:[19550]” dev=”pipefs”➥ ino=19550 scontext=system_u:system_r:docker_apache_t:s0:c740,c787➥ tcontext=system_u:system_r:init_t:s0 tclass=fifo_filetype=AVC msg=audit(1433073250.236:394): avc: denied { append} for➥ pid=2379 comm=”httpd” dev=”pipefs” ino=19551➥ scontext=system_u:system_r:docker_apache_t:s0:c740,c787➥ tcontext=system_u:system_r:init_t:s0 tclass=fifo_filetype=AVC msg=audit(1433073250.236:394): avc: denied { open} for➥ pid=2379 comm=”httpd” path=”pipe:[19551]” dev=”pipefs” ino=19551➥ scontext=system_u:system_r:docker_apache_t:s0:c740,c787➥ tcontext=system_u:system_r:init_t:s0 tclass=fifo_file[…]
(1)在审计日志中的消息类型永远是AVC代表SELinux违反行为,时间戳表示为时代开始(定义为1970年1月1日)以来的秒数
(2)花括号内展示了被拒绝的行为类型
(3)处罚违反行为的进程ID和命令名
(4)目标文件的路径、设备和i-node
(5)目标的SELinux上下文
(6)目标对象的类别
这里好多术语,我们没时间教读者关于SELinux的一切。如果读者想了解更多,可以从Red Hat的SELinux文档开始:http://mng.bz/QyFh。
就现在来说,用户需要检查这些违反行为没有预见外的。什么是预见外的?例如,程序试图打开一个用户没打算让它打开的端口或者文件。接下来就要好好考虑我们要教的了:通过一个新的SELinux模块给这些违反行为打补丁。
在本例中,我们很高兴看到httpd可以写管道。我们已经弄明白了SELinux在拒绝做什么,因为提到的“拒绝”行为是向虚拟机的管道文件执行append、wirte和open。
4.给SELinux违反行为打补丁
一旦确定了看到的违反行文是可以接受的,有些工具就可以自动生成要应用的策略文件,因此就不用犯难又犯险地自己去写了。接下来了的例子用了audit2allow工具来达成这一点:
mkdir -p /root/selinux_policy_httpd_auto (1)cd /root/selinux_policy_httpd_auto (2)audit2allow -a –w (3)audit2allow -a -M newmodname create policysemodule -i newmodname.pp (4)
(1)创建一个用来存储新的SELinux模块的新目录
(2)使用audit2allow工具来展示要通过读取审计日志生成的策略。检查一遍它的合理性
(3)用-M标志和你为模块选的名字来创建模块
(4)通过新创建的.pp文件来安装模块
重要的是要明白,我们新创建的这个SELinux模块,通过引用并且给docker_apache_t类型增加权限,“包含”(或者“需要”)并且改变了我们之前创建的那个模块。把二者结合到一个完整并独立的.te策略文件里就留给读者作为练习。
5.测试新模块
安装好了新模块,就可以试一下重新启用SELinux并重启容器。
无法把强制模式设为宽容模式? 如果之前无法把宿主机设为
permissive模式(而且在原始的docker_apache.te文件里加入了讨论过的那一行),那么在继续之前要重新编译和重新安装原始的docker_apache.te文件(带上讨论过的那一行)。
docker rm -f selinuxdocksetenforce Enforcingdocker run -d —name selinuxdock \—security-opt label:type:docker_apache_t httpddocker logs selinuxdockgrep -w denied /var/log/audit/audit.log
审计日志中应该没有错误。应用程序在SELinux上下文的管控中启动了。
SELinux以复杂且难以管理闻名,有一句流传甚广的抱怨说人们经常关了它而不是调试它。这一点很不安全。虽然SELinux好的方面需要认真努力才能掌握,但是我们希望本技巧展示了在Docker不是开箱即用的情况下,如何创建一份安全专家可以审查乃至批准的东西。
10.4 小结
本章中我们从不同的角度解决了Docker中的安全问题。我们讨论了Docker中安全的基本问题,展示了解决这些问题的方式。你需要或者想要什么样的东西,取决于所在组织的性质以及对自己的用户的信任程度。
本章涉及以下内容:
- 使用SELinux降低容器以root身份运行的风险;
- 通过HTTP对Docker API的用户进行鉴权;
- 用证书为Docker API加密;
- 限制容器内root用户的能力;
- 使用应用程序平台即服务(aPaaS)来控制对Docker运行时的访问。
现在读者应该充分了解了Docker带来的安全问题,并了解了如何减轻它们。
接下来我们要把Docker带入生产环境,看一下在把Docker作为在线运维的一部分应该考虑的一些方面。
