gitbook/安全攻防技能30讲/docs/188172.md
2022-09-03 22:05:03 +08:00

185 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 15 | Docker安全在虚拟的环境中就不用考虑安全了吗
你好,我是何为舟。
在[13讲](https://time.geekbang.org/column/article/186777)中我们讲了Linux系统安全。但是当你在和同事讨论Linux系统安全的时候同事表示公司的服务都是通过Docker来进行容器化部署的。开发在操作中并不会接触实际的Linux服务器所以不会去关注Linux安全 。而且因为容器是隔离的就算容器被黑客攻击了也只是容器内部受到影响对宿主的Linux系统和网络都不会产生太大影响。
事实上我知道很多人都有这种想法。但是你在学习了安全专栏之后可以试着思考一下开发使用了Docker就一定安全吗真的可以不用考虑安全问题了吗
以防你对Doker还不是很了解在解决这些问题之前我先来解释一下这节课会涉及的3个概念帮你扫清概念障碍。
* Docker服务Docker所提供的功能以及在宿主机Linux中的Docker进程。
* Docker镜像通过Dockerfile构建出来的Docker镜像。
* Docker容器实际运行的Docker容器通常来说一个Docker镜像会生成多个Docker容器。Docker容器运行于Docker服务之上。
了解了这3个关键概念之后我们今天就从这些概念入手来谈一谈Docker的安全性。
## Docker服务安全
我们首先来看Docker服务的安全性。Docker服务本身需要关注的安全性就是隔离。如果黑客在控制了容器之后能够成功对宿主机产生影响就说明黑客突破了Docker服务的隔离保护也就是我们所说的“Docker逃逸”。
那么Docker服务是如何对容器进行隔离来防止“Docker逃逸”的呢接下来我就介绍一下这3个关键的隔离机制Namespace机制、Capabilities机制和CGroups机制。
第1个是**Namespace机制**。
我们知道Docker之所以广泛流行是因为它提供了一种轻量化的隔离环境也就是容器。
下面我们重点解释一下“轻量化”和“隔离”这两个词。首先是轻量化。怎么理解轻量化呢我们可以对比虚拟机来进行理解。虚拟机是自己创造了一个虚拟内核让这个虚拟内核去和虚拟机的进程进行沟通然后虚拟内核再和真实的Linux内核进行沟通。而Docker提供的容器简化了这个沟通过程让Docker中的进程直接和Linux内核进行沟通。
![](https://static001.geekbang.org/resource/image/6b/17/6b2a1d0ff075f7f1cbe4a70d6a211617.jpeg)
第二个词是隔离。也就是说Docker提供的容器环境是和Linux内核隔离的。想要实现这种隔离就需要用到**Namespace机制**了。所以这里我先给你简单解释一下什么是Namespace机制。
Namespace是Linux提供的一种标签机制Linux内核会对不同Namespace之间的进程做隔离避免不同的进程之间互相产生影响。所以Docker服务会为每一个Docker容器创建一个单独的Namespace空间。 这样一来不同容器之间、容器和系统之间都是不同的Namespace也就实现了隔离。
这种基于Namespace的隔离我一般叫它“伪隔离”。因为通过Namespace进行的隔离并不彻底。为啥这么说呢Docker容器在隔离的环境中仍然需要使用一些底层的Linux进程和设备支持。比如你在Docker容器中仍然需要使用鼠标、键盘等输入输出设备那么容器就必须挂载Linux系统中的/sys来获得对应的驱动和配置信息。也就是说你在Docker中看到的/sys目录实际就是Linux系统中的/sys目录。类似地还有一些没有被Namespace隔离开的目录和模块包括以下这些内容
* 部分的进程目录/proc/…
* 内存映像/dev/mem
* 系统设备/dev/sd\*
* Linux内核模块
换一句话说因为容器和宿主机需要共同使用一些服务比如容器和宿主机使用的是同一个鼠标所以上面的这些目录和模块对于容器和宿主机来说其实是共享的。从理论上来说如果你在Docker容器中修改了这些目录那么宿主机当中也会同步相应的修改结果。
第2个**Capabilities机制**。
我们刚刚说了Namespace的伪隔离机制让容器和宿主机共享部分目录。那么这是不是也意味着Docker容器可以通过这些目录来影响宿主机从而实现“Docker逃逸”呢为了避免这种情况Docker服务使用了Capabilities机制来限制容器的操作。
Capabilities提供了更细粒度的授权机制它定义了主体能够进行的某一类操作。比如一个Web服务需要绑定80端口但80端口的绑定是需要ROOT权限的。为了防止ROOT权限滥用Docker会通过Capabilities给予这个Web服务net\_bind\_service这个权限允许绑定到小于1024的端口。同样地Docker对容器的ROOT也加了很多默认的限制比如
* 拒绝所有的挂载操作;
* 拒绝部分文件的操作,比如修改文件所有者;
* 拒绝内核模块加载。
这里有一点需要你注意Capabilities对容器可进行操作的限制程度很难把控。这是因为过松会导致Docker容器影响宿主机系统让Docker隔离失效过严会让容器和容器内的服务功能受限无法正常运行。
所以在默认情况下Docker会采用白名单机制白名单列表你可以在Docker源码中查看进行限制即只允许Docker容器拥有几个默认的能力。那有了白名单限制即使黑客成功拿到了容器中的ROOT权限能够造成的影响也相对较小。所以我们常说“Docker逃逸”是一件不容易的事情。
第3个是**CGroups机制**。
好了现在你应该知道Docker服务本身是如何防止“Docker逃逸”的了。作为一个容器Docker显然不能过多地占用宿主机资源不然对宿主机和自身的可用性都会产生影响。那Docker是如何实现资源限制的呢
Docker服务可以利用CGroups机制来实现对容器中内存、CPU和IO等的限制。比如通过下面的命令我们就可以限制Docker容器只使用2个CPU和100MB的内存来运行了。
```
docker run -it --cpus=2 --memory="100m" ubuntu:latest /bin/bash
```
所以当一个宿主机中运行了多个Docker容器的时候我们可以通过CGroups给每一个容器弹性地分配CPU资源。同样地这个限制既不能过松过松会导致某一个Docker容器耗尽宿主机资源也不能过严过严会使得容器内的服务得不到足够的资源支持。这都需要我们自己经过慎重考量来进行配置没有默认的安全机制可以辅助我们。
现在你应该已经了解Docker服务中的3个主要机制了。这里我把这3个主要机制的特点总结成了一张表格帮助你加深理解。
![](https://static001.geekbang.org/resource/image/70/6b/70dbe21875f958841c62213e76ed6e6b.jpeg)
## Docker守护进程
想要运行Docker镜像就必须先启动Docker的Daemon守护进程。而启动这个守护进程需要ROOT权限。因此守护进程本身如果出现漏洞就会给黑客提供一个权限提升的入口。那通过这个守护进程黑客能进行哪些操作呢
首先作为守护进程Daemon具备操控Docker容器的全部权限。这也就意味着黑客可以任意地上线和下线容器、运行黑客自己的镜像、篡改已有镜像的配置等。这么说可能不够直观我来详细解释一下。黑客通过守护进程可以将宿主机的根目录共享到镜像中这样一来镜像就可以对宿主机的目录进行任意的修改了。另外除了影响正常的线上容器黑客还能够通过简单的docker exec命令获取容器环境中的Shell从而执行任意命令了 。
那么黑客怎么才能控制Daemon守护进程呢最简单的方法当然是直接进入宿主机通过Docker命令进行交互。但如果黑客已经进入宿主机还去操控容器就是多此一举了。所以黑客主要是通过远程API来对Docker守护进程发起攻击。
守护进程提供的API接口是为了方便用户去做一些自动化的工具来操控Docker容器。而在默认情况下这个API接口不需要进行认证。你可以尝试探测一下你的公司内外网中是否存在开放的2375端口守护进程API默认监听的端口。如果存在的话那么你基本上就能够控制这台服务器的Docker守护进程了。
为了避免这种无认证的情况发生Docker提供了证书的方式来进行认证。开启API接口的命令如下所示
```
dockerd --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem -H=0.0.0.0:2376
```
通过以上命令我们就能够在宿主机开启远程API接口。在客户端中只需要提供相应的证书信息就能够完成经过认证的API接口调用了。
```
curl https://127.0.0.1:2376/images/json --cert cert.pem --key key.pem --cacert ca.pem
```
那通过这样的配置我们就能解决了API接口的认证问题也就提升了Docker守护进程的安全性。
## Docker镜像安全
了解了Docker守护进程的安全风险和防护方法之后我们再来看一下Docker镜像的安全。
对于Docker镜像来说它本身就是一个模拟的操作系统自然也会存在操作系统中的各类安全威胁和漏洞。但是由于一个Docker镜像一般只会运行某一种服务也就相当于一个操作系统中只有一个用户。因此Docker镜像面临的安全威胁也会小很多。
接下来我就为你详细讲解两种保证Docker镜像安全的方式分别是“使用最精简的镜像”和“最小权限原则”。
### 使用最精简的镜像
前面我们讲了Docker镜像的概念我们知道Docker镜像是通过Dockerfile来构建的。而Dockerfile构建的第一句是FROM \*\*\*。以Node.js的环境为例你的基础镜像可能是node那么Dockerfile的第一行应该是FROM node。
```
FROM node
COPY . ./
EXPOSE 8080
CMD [“node”, “index.js”]
```
这个基础的node镜像实际包含了一个完整的操作系统但是在实际应用中有大部分的系统功能我们是用不到的。而这些用不到的系统功能却正好为黑客提供了可乘之机。
Snyk在2019年的[Docker漏洞统计报告](https://snyk.io/blog/top-ten-most-popular-docker-images-each-contain-at-least-30-vulnerabilities/)称最热门的10个Docker基础镜像包含的已知系统漏洞最少的有30个最多的有580个。
![](https://static001.geekbang.org/resource/image/58/a7/58e23d540b2481262d17d40395f8dca7.jpeg)
这是非常惊人的。通过一句简单的FROM node就能让你的Docker镜像中引入580个系统漏洞。那我们该如何避免引入漏洞呢这个时候我们就需要使用精简版的基础镜像了。一般来说精简版的Docker镜像标签都会带有slim或者alpine。
比如说如果你采用node:10-slim那么漏洞数会降低到71个。如果使用node:10-alpine那么已知的漏洞数会降为0。之所以会发生这种现象是因为使用精简版的基础镜像可以去除大部分无用的系统功能和依赖库所以存在于这些功能中的漏洞自然也就被剔除了。
因此对于Docker来说通过使用精简的基础镜像去除一些无用的系统功能既能够降低最终镜像的体积又能够降低安全风险何乐而不为呢
### Docker中的最小权限原则
除此之外我们在Linux操作系统中提到的最小权限原则在Docker镜像中同样适用。
这是因为在默认情况下容器内的进程都是以ROOT权限启动的。而Docker又是伪隔离所以容器就和宿主机拥有一致的ROOT权限了。虽然Docker通过Capabilities对容器内的ROOT能力进行了限制。但是使用ROOT权限去运行一个普通的服务很不合适。为此我们可以通过USER关键词来使用一个低权限的用户运行服务。
以Node.js为例在node的基础镜像中默认创建了node这么一个具备较小权限的用户。因此我们可以在Dockerfile中加入一行USER node来使用这个最小权限用户。
```
FROM node:10-alpine
...
USER node
CMD [“node”, “index.js”]
```
当然如果有的基础镜像本身不提供额外的用户你就需要自己创建一个了。以ubuntu为例我们可以通过groupadd和useradd创建一个node用户这个用户没有密码、没有home目录、也没有shell就是一个最小权限用户。Dockerfile的内容如下
```
FROM ubuntu
RUN groupadd -r node && useradd -r -s /bin/false -g node node
...
USER node
CMD node index.js
```
现在你应该已经知道Docker镜像的两种安全防护方法了我来简单总结一下。第一个是通过使用最精简的基础镜像来删减Docker镜像中不必要的功能从而降低出现漏洞的概率。第二个则是采取最小权限原则以低权限用户来执行服务限制黑客的能力。
## 总结
好了,今天的内容讲完了。我们来一起总结回顾一下,你需要掌握的重点内容。
今天我主要通过Docker服务、Docker守护进程和Docker镜像这三个方面带你学习Docker的安全性。
在Docker服务中主要是利用Namespace、Capabilities和CGroups机制来对Docker容器进行各种隔离和限制在Docker守护进程中我们通过给远程API加上认证功能来保证安全性在Docker镜像中我们主要是通过最小镜像和最小权限的原则去提升镜像本身的安全性。
在实际对Docker进行安全防护的过程中我们也可以采取各类针对Docker的扫描工具来发现问题。比如[C](https://github.com/quay/clair)[lair](https://github.com/quay/clair),它会对你的镜像进行静态的扫描分析,并和漏洞库进行比对,从而发现镜像中可能存在的安全漏洞。
以Docker为代表的容器技术可以说是现在应用开发中最常见的技术了。很多开发人员现在甚至不用使用原始的Linux系统直接基于Docker进行开发就好了。因此我们在开发应用的过程中要时刻关注Docker的安全性。
好了,我把这一讲的重点内容梳理了一个脑图。你可以用它来查漏补缺,也可以自己来梳理看看,加深印象。
![](https://static001.geekbang.org/resource/image/c6/7c/c6d3f48513a247437372b2eb717f797c.jpg)
## 思考题
最后,给你留一个思考题。
“容器上云”是目前普遍的技术趋势,你是否有使用过一些容器云的产品?可以研究一下,在容器云中,云平台给容器设置了哪些安全限制。
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!