第15章:Docker安全

好的安全性是基于分层隔离的,而Docker恰好有很多分层。Docker 支持所有主流Linux安全机制,同时 Docker 自身还提供了很多简单的并且易于配置的安全技术。安全本质就是分层!通俗地讲,拥有更多的安全层,就能拥有更多的安全性。而 Docker 提供了很多安全层。下图展示了一部分安全技术。
 

深入浅出Docker 深入浅出docker 豆瓣_深入浅出Docker

Linux Docker 利用了大部分 Linux 通用的安全技术。这些技术包括了命名空间(Namespace)、控制组(CGroup)、系统权限(Capability),强制访问控制(MAC)系统以及安全计算(Seccomp)。对于上述每种技术,Docker 都设置合理的默认值,实现了流畅的并且适度安全的开箱即用体验。同时,Docker 也允许用户根据特定需求自定义调整每项安全配置。Docker 平台本身也提供了一些非常棒的原生安全技术。并且重要的是,这些技术使用起来都很简单!1) Docker Swarm 模式:默认是开启安全功能的。无须任何配置就可以获得加密节点 ID、双向认证、自动化 CA 配置、自动证书更新、加密集群存储、加密网络等安全功能。2) Docker 内容信任(Docker Content Trust, DCT):允许用户对镜像签名,并且对拉取的镜像的完整度和发布者进行验证。3) Docker 安全扫描(Docker Security Scanning):分析 Docker 镜像,检查已知缺陷并提供对应的详细报告。4) Docker 密钥:使安全成为 Docker 生态系统中重要的一环。Docker 密钥存储在加密集群存储中,在容器传输过程中实时解密,使用时保存在内存文件系统,并运行了一个最小权限模型。Docker 在使用主流 Linux 安全技术的同时,还提供了额外的扩展以及一些新的安全技术。Linux 安全技术看起来可能略为复杂,但是 Docker 平台的安全技术却非常简单。

Docker 平台提供的绝大部分安全功能使用起来都很简单,且大部分的安全设置都配有默认值,意味着无须任何配置,就能得到一个相当安全的平台。当然默认配置不一定是最合适的,但至少在最开始能够保障一定的安全性。如果默认配置与用户需求不符,那么用户也可以进行自定义配置。下面主要对 Docker 中用到的主要 Linux 技术进行简要介绍。1)Namespace:内核命名空间属于容器中非常核心的一部分, 该技术能够将操作系统(OS)进行拆分,使一个操作系统看起来像多个互相独立的操作系统一样。这种技术可以用来做一些非常酷的事情,比如在相同的 OS 上运行多个 Web 服务,同时还不存在端口冲突的问题。该技术还允许多个应用运行在相同 OS 上并且不存在竞争,同时还能共享配置文件以及类库。比如用户可以在相同的 OS 上运行多个 Web 服务,每个端口都是 443。为了实现该目的,可以将两个 Web 服务应用分别运行在自己的网络命名空间中。这样可以生效的原因是每个网络命名空间都拥有自己的 IP 地址以及对应的全部端口。也可能需要将每个 IP 映射到 Docker 主机的不同端口之上,但是使用 IP 上的哪个端口则无须其他额外配置。用户还可以运行多个应用,应用间共享类库和配置文件,但是版本可能不同。为了实现该目标,需要在自己的挂载命名空间中运用每个应用程序。这样做能生效的原因,是每个挂载命名空间内都有系统上任意目录的独立副本。Linux Docker 现在利用了下列内核命名空间。1)进程ID(PID),2)网络(NET),3)文件系统/挂载(MNT),4)进程内通信(IPC)5)用户,6)UTS。   Namespace:Docker 容器是由各种命名空间组合而成的,Docker 容器本质就是命名空间的有组织集合。例如,每个容器都由自己的 PID、NET、MNT、IPC、UTS 构成,还可能包括 USER 命名空间。这些命名空间有机的组合就是所谓的容器。下图展示了两个运行在相同 Linux 主机上的容器。

深入浅出Docker 深入浅出docker 豆瓣_Docker_02

1) 进程 ID 命名空间:Docker 使用 PID 命名空间为每个容器提供互相独立的容器树。每个容器都拥有自己的进程树,意味着每个容器都有自己的 PID 为 1 的进程。PID 命名空间也意味着容器不能看到其他容器的进程树,或者其所在主机的进程树。   2) 网络命名空间:Docker 使用 NET 命名空间为每个容器提供互相隔离的网络栈。网络栈中包括接口、ID 地址、端口地址以及路由表。例如,每个容器都有自己的 eth0 网络接口,并且有自己独立的 IP 和端口地址。  3) 挂载点命名空间:每个容器都有互相隔离的根目录 /。这意味着每个容器都有自己的 /etc、/var、/dev 等目录。容器内的进程不能访问 Linux 主机上的目录,或者其他容器的目录,只能访问自己容器的独立挂载命名空间。   4) 进程内通信命名空间:Docker使用IPC命名空间在容器内提供共享内存。IPC提供的共享内存在不同容器间也是互相独立的。5) 用户命名空间:Docker 允许用户使用 USER 命名空间将容器内用户映射到主机不同的用户上。常见的如将容器内的root用户映射到非 root 用户上。用户命名空间对于 Docker 来说还属于新生事物且非必选项。该部分内容在未来可能出现改变。  6) UTS 命名空间:Docker 使用 UTS 命名空间为每个容器提供自己的主机名称。     Control Group:如果说命名空间用于隔离,那么控制组就是用于限额。假设容器就是酒店中的房间。每个容器间都是互相独立的,但是每个房间都共享一部分公共资源,比如供应水电、共享游泳池、共享健身、共享早餐餐吧等。CGroup 允许用户设置一些限制来保证不会存在单一容器占用全部的公共资源,如用光全部水或者吃光早餐餐吧的全部食物。在 Docker 的世界中,容器之间是互相隔离的,但却共享 OS 资源,比如 CPU、RAM 以及硬盘 I/O。CGroup 允许用户设置限制,这样单个容器就不能占用主机全部的 CPU、RAM 或者存储 I/O 资源了。  Capability:以 root 身份运行容器不是什么好主意,root 拥有全部的权限,因此很危险。但是如果以非 root 身份在后台运行容器的话,缺少权限处处受限。所以用户需要一种技术,能选择容器运行所需的 root 用户权限。在底层,Linux root 用户是由许多能力组成的。其中一部分包括以下几点。1)CAP_CHOWN:允许用户修改文件所有权。2)CAP_NET_BIND_SERVICE:允许用户将socket绑定到系统端口号。3)CAP_SETUID:允许用户提升进程优先级。4)CAP_SYS_BOOT:允许用户重启系统。Docker 采用 Capability 机制来实现用户在以 root 身份运行容器的同时,还能移除非必须的 root 能力。如果容器运行只需要 root 的绑定系统网络端口号的能力,则用户可以在启动容器的同时移除全部 root 能力,然后再将 CAP_NET_BIND_SERVICE 能力添加回来。MAC:Docker 采用主流 Linux MAC 技术,如 AppArmor 以及 SELinux。基于用户的 Linux 发行版本,Docker 对新容器增加了默认的 AppArmor 配置文件。根据 Docker 文档的描述,默认配置文件提供了“适度的保护,同时还能兼容大部分应用”。Docker 允许用户在启动容器的时候不设置相应策略,还允许用户根据需求自己配置合适的策略。Seccomp:Docker 使用过滤模式下的 Seccomp 来限制容器对宿主机内核发起的系统调用。按照 Docker 的安全理念,每个新容器都会设置默认的 Seccomp 配置,文件中设置了合理的默认值。这样做是为了在不影响应用兼容性的前提下,提供适度的安全保障。用户同样可以自定义 Seccomp 配置,同时也可以通过向 Docker 传递指定参数,使 Docker 启动时不设置任何 Seccomp 配置。

Linux 安全技术总结:Docker 基本支持所有的 Linux 重要安全技术,同时对其进行封装并赋予合理的默认值,这在保证了安全的同时也避免了过多的限制,如下图所示。
 

深入浅出Docker 深入浅出docker 豆瓣_命名空间_03

自定义设置某些安全技术会非常复杂,因为这需要用户深入理解安全技术的运作原理,同时还要了解 Linux 内核的工作机制。Docker 内容信任允许用户对内容进行签名和认证,密钥目前也是 Docker 中的一等公民。Docker 为这些安全技术设定了合理的默认值,但是用户也可以自行修改配置,或者禁用这些安全技术。

Swarm 模式是 Docker 未来的趋势。Swarm 模式支持用户集群化管理多个 Docker 主机,同时还能通过声明式的方式部署应用。每个 Swarm 都由管理者和工作者节点构成,节点可以是 Linux 或者 Windows。管理者节点构成了集群中的控制层,并负责集群配置以及工作负载的分配。工作者节点就是运行应用代码的容器。正如所预期的,Swarm 模式包括很多开箱即用的安全特性,同时还设置了合理的默认值。这些安全特性包括以下几点。1) 加密节点 ID。2) 基于 TLS 的认证机制。3) 安全准入令牌。4)支持周期性证书自动更新的 CA 配置。5)加密集群存储(配置 DB)。6)加密网络。举例说明环境布置如下,例中 3 个 Docker 主机分别叫作“mgr1”“mgr2”“wrk1”。每台主机上都安装 Ubuntu 16.04,其上运行了 Docker 18.01.0-ce。同时还有一个网络负责联通 3 台主机,并且主机之间可以通过名称互相 ping 通。可以在其 Swarm 集群管理者节点上运行下面的命令。“mgr1”被配置为 Swarm 集群中的第一个管理节点,也是根 CA 节点。Swarm 集群已经被赋予了加密 Swarm ID,同时“mgr1”节点为自己发布了一个客户端认证信息,标明自己是Swarm集群管理者。证书的更新周期默认设置为 90 天,集群配置数据库也已经配置完成并且处于加密状态。安全令牌也已经成功创建,允许新的管理者和工作者节点加入到 Swarm 集群中。实验环境如下图所示。
 

深入浅出Docker 深入浅出docker 豆瓣_Docker_04

现在将“mgr2”节点加入到集群中,作为额外的管理者节点。将新的管理者节点加入到 Swarm 需要两步。第一步,需要提取加入管理者到集群中所需的令牌;第二步在“mgr2”节点上执行 docker swarm join 命令。只要将管理者准入令牌作为 docker swarm join 命令的一部分,“mgr2”作为管理者节点加入 Swarm。在“mgr1”上获取管理者准入令牌$ docker swarm join-token manager   复制该命令并在“mgr2”节点上运行。$ docker swarm join --token SWMTKN-1-1dmtwu...r17stb-2axi5...8p7glz \
> 172.31.5.251:2377   “mgr2”现在已经作为另一个管理者加入 Swarm。join 命令的格式是 docker swarm join --token <manager-join-token> <ip-of-existing-manager>:<swarm-port>。可以通过在任意管理者节点上运行 docker node ls 命令来确认上述操作。$ docker node ls 确认“mgr1”和“mgr2”都加入了 Swarm,并且都是 Swarm 管理者。两个管理者这个数量,大概是最糟糕的一种情况了。但是这只是一个实验环境,而不是什么核心业务生产环境,所以糟糕点也无所谓。向 Swarm 中加入工作者也只需两步。第一步需要获取新工作者的准入令牌,第二步是在工作者节点上运行 docker swarm join 命令。目前已经拥有包含两个管理者和一个工作者的 Swarm 集群。管理者配置为高可用(HA),并且复用集群存储。最新的配置如下图所示。
 

深入浅出Docker 深入浅出docker 豆瓣_Docker_05

 

到目前为止,已经成功搭建了安全的 Swarm 集群。具体分析背后涉及的安全技术。1) Swarm 准入令牌:向某个现存的 Swarm 中加入管理者和工作者所需的唯一凭证就是准入令牌。因此,保证准入令牌的安全十分关键,不要将其发布到公开的库中。每个 Swarm 都包含两种不同准入令牌。管理者令牌和工作者所需准入令牌。每个准入令牌都由 4 个不同的字段构成,中间采用虚线(-)连接。PREFIX - VERSION - SWARM ID - TOKEN。PREFIX 永远是“SWMTKN”,这样允许工作者通过表达式匹配到该令牌,以避免意外将其发布到公共环境当中;VERSION 这一列则展示了 Swarm 的版本信息;SWARM ID 列是 Swarm 认证信息的一个哈希值;TOKEN 这一列的内容决定了该令牌是管理者还是工作者的准入令牌。如果用户认为当前准入令牌存在风险,仅用一条命令就可以取消该准入令牌授权,同时发布新的准入令牌。如下取消已经授权的管理者准入令牌,之后又发布了新的令牌。$ docker swarm join-token --rotate manager     docker swarm join --token \    SWMTKN-1-1dmtwu...r17stb-1i7txlh6k3hb921z3yjtcjrc7 \   需要注意的是,新旧令牌只有最后字段存在区别。SWARM ID 还是相同的。准入令牌保存在集群配置的数据库中,默认是加密的。2) TLS 和双向认证:每个加入 Swarm 的管理者和工作者节点,都需要发布自己的客户端证书。这个证书用于双向认证。证书中定义了节点相关信息,包括从属的 Swarm 集群以及该节点在集群中的身份(管理者还是工作者)。3) 配置一些 CA 信息:通过 docker swarm update 命令可以配置 Swarm 证书的更新周期。下面的示例中,将 Swarm 的证书更新周期修改为 30 天。$ docker swarm update --cert-expiry 720h     Swarm 允许节点在证书过期前重新创建证书,这样可以保证 Swarm 中全部节点不会在同一时间尝试更新自己的证书信息。可以在创建 Swarm 的时候,通过在 docker swarm init 命令中增加 --external-ca 参数来指定外部的 CA。docker swarm ca 命令可以用于管理 CA 相关配置。可以在运行该命令时指定 --help 来查看命令功能。$ docker swarm ca --help  4) 集群存储:集群存储是 Swarm 的大脑,保存了集群配置和状态数据。存储目前是基于 etcd 的某种实现,并且会在 Swarm 内所有管理者之间自动复制。存储默认也是加密的,Docker 网络和 Docker 密钥都用到了集群存储。

Docker 安全扫描:快速发现代码缺陷的能力至关重要。Docker 安全扫描功能使得对 Docker 镜像中已知缺陷的检测工作变得简单。Docker 安全扫描已经可以用于 Docker Hub 上私有仓库的镜像了。同时该技术还可以作为 Docker 可信服务本地化部署解决方案的一部分。最后,所有官方 Docker 镜像都经过了安全扫描,扫描报告在其仓库中可以查阅。Docker 安全扫描对 Docker 镜像进行二进制代码级别的扫描,对其中的软件根据已知缺陷数据库(CVE 数据库)进行检查。在扫描执行完成后,会生成一份详细报告。打开浏览器访问 Docker Hub,并搜索 Alpine 仓库。下图展示了官方 Alpine 仓库的 Tags 标签页。
 

深入浅出Docker 深入浅出docker 豆瓣_命名空间_06

Alpine 仓库是官方仓库,这意味着该仓库会自动扫描并生成对应报告。可以看到,镜像标签为 edge、lates 以及 3.6 的镜像都通过了已知缺陷的检查。但是 alpine:3.5 镜像存在已知缺陷(标红)。如果打开 alpine:3.5 镜像,可以看详细信息。这是发现自己软件中已知缺陷详情的一种简单方式。Docker 可信镜像仓库服务(Docker Trusted Registry, DTR),属于 Docker 企业版中本地化镜像仓库服务的一部分内容,提供了相同的 Capability,同时还允许用户自行控制其镜像扫描时机以及扫描方式。

Dockr 内容信任(Docker Content Trust,DCT)使得用户很容易就能确认所下载镜像的完整性以及其发布者。在不可信任的网络环境中下载镜像时,这一点很重要。从更高层面来看,DCT 允许开发者对发布到 Docker Hub 或者 Docker 可信服务的镜像进行签名。当这些镜像被拉取的时候,会自动确认签名状态。下图展示了这一过程。
 

深入浅出Docker 深入浅出docker 豆瓣_深入浅出Docker_07

DCT 还可以提供关键上下文,如镜像是否已被签名从而可用于生产环境,镜像是否被新版本取代而过时等。DTC 提供的上下文还在初期,配置起来相当复杂。在 Docker 主机上启用 DCT 功能,所要做的只是在环境中将 DOCKER_CONTENT_TRUST 变量设置为 1。$ export DOCKER_CONTENT_TRUST=1,一旦 DCT 功能开启,就不能获取并使用未签名镜像了。Docker 内容信任能帮助检查从 Docker 服务中拉取的镜像。该技术的基础模式配置起来非常简单,但是类似上下文等一些高级特性,现阶段配置还是非常复杂的。

Docker 密钥:很多应用都需要密钥。比如密码、TLS 证书、SSH key 等。在 Docker1.13 版本之前,没有一种标准且安全的方式能让密钥在应用间实现共享。常见的方式是开发人员将密钥以文本的方式写入环境变量。这与理想状态差距甚远。Docker1.13 引入了 Docker 密钥,将密钥变成 Docker 生态系统中的一等公民。例如,增加了一个新的子命令 docker secret 来管理密钥。在 Docker 的 UCP 界面中,也有专门的地方来创建和管理密钥。在后台,密钥在创建后以及传输中都是加密的,使用时被挂载到内存文件系统,并且只对那些已经被授权了的服务开放访问。这确实是一种综合性的端到端解决方案。下图展示了其总体流程。
 

深入浅出Docker 深入浅出docker 豆瓣_docker_08

1) 密钥被创建并发送到 Swarm。  2) 密钥存放在集群存储当中,且是加密的(每个管理者节点都能访问集群存储)。
3) B 服务被创建,并且使用了该密钥。  4) 密钥传输到 B 服务的任务节点(容器)的过程是加密的。
5) B 服务的容器将密钥解密并挂载到路径 /run/secrets 下。这是一个临时的内存文件系统(Windows中无内存文件系统这个概念,因此步骤会有差异)。
6) 一旦容器(服务任务)完成,内存文件系统关闭,密钥也随之删除。    7) A 服务中的容器不能访问该密钥。
可以通过 docker secret 子命令来管理密钥,通过在运行 docker service create 命令时附加 --secret为某个服务指定密钥。