天天看点

12 个优化 Docker 镜像安全性的技巧

作者 | Marius

译者 | 王强

策划 | 闫园园

本文介绍了 12 个优化 Docker 镜像安全性的技巧。每个技巧都解释了底层的攻击载体,以及一个或多个缓解方法。这些技巧包括了避免泄露构建密钥、以非 root 用户身份运行,或如何确保使用最新的依赖和更新等。

1

前言

当你是刚开始使用 Docker 的新手时,你很可能会创建不安全的 Docker 镜像,使攻击者很容易借此接管容器,甚至可能接管整个主机,然后渗透到你公司的其他基础设施中。

可以被滥用来接管你的系统的攻击向量有很多,例如:

启动的应用程序(在你 Dockerfile 的 ENTRYPOINT 中指定)以 root 用户身份运行。这样以来,一旦攻击者利用了一个漏洞并获得了 shell 权限,他们就可以接管 Docker 守护程序所运行的主机。

你的镜像是基于一个过时的和 / 或不安全的基础镜像,其中包含(现在)众所周知的安全漏洞。

你的镜像包含了一些工具(如 curl、apt 等),一旦攻击者获得了某种访问权,就可以通过这些工具将恶意软件加载到容器中。

下面的各个章节讲解了能够优化你的镜像安全性的各种方法。它们是按重要性 / 影响程度排序的,也就是说排名靠前的方法更重要。

2

避免泄露构建密钥

构建密钥是只在构建 Docker 镜像时需要的凭证(不是在运行时)。例如,你可能想在你的镜像中包含某个应用程序的一个编译版本,这个应用的源代码是闭源的,并且其 Git 存储库是有访问保护的。在构建镜像时,你需要克隆 Git 存储库(这需要构建密钥,例如该存储库的 SSH 访问密钥),从源代码构建应用程序,然后再删除源代码(和密钥)。

“泄露“构建密钥是说你不小心把这种密钥烘焙到了你的镜像的某个层中。这种情况很严重,因为拉取你的镜像的所有人都可以检索到这些机密。这个问题源于这样一个事实,即 Docker 镜像是以纯粹的加法方式逐层构建的。你在一个层中删除的文件只是被“标记”为已删除,但拉取你镜像的人们仍然可以使用高级工具访问它们。

可以使用以下两种方法之一来避免泄露构建密钥。

多阶段构建

Docker 多阶段构建(官方文档)有许多用例,例如加快你的镜像构建速度,或减少镜像大小。本系列的其他文章会详细介绍其他用例。总之,你也可以通过多阶段构建来避免泄露构建密钥,如下所示:

创建一个阶段 #A,将凭证复制到其中,并使用它们来检索其他工件(例如上述例子中的 Git 存储库)和执行进一步的步骤(例如编译一个应用程序)。阶段 #A 的构建确实包含了构建的密钥!

创建一个 #B 阶段,其中你只从 #A 阶段复制非加密的工件,例如一个已编译的应用程序。

只发布 / 推送阶段 #B 的镜像

BuildKit 的密钥

背景知识

如果你使用 docker build 进行构建,可以实际执行构建的后端选项不止一个。其中较新和较快的后端是 BuildKit,你需要在 Linux 上设置环境变量 DOCKER_BUILDKIT=1 来显式启用它。注意,BuildKit 在 Windows/MacOS 的 Docker for Desktop 上是默认启用的。

正如这里的文档所解释的(阅读它们以了解更多细节),BuildKit 构建引擎支持 Dockerfile 中的额外语法。要使用构建密钥,请在你的 Dockerfile 中放入类似下面这样的内容:

当 RUN 语句被执行时,密钥将对这个构建容器可用,但不会将密钥本身(这里是:/foobar 文件夹)放入构建的镜像中。你需要在运行 docker build 命令时指定密钥的源文件 / 文件夹(位于主机上)的路径,例如:

不过有一点需要注意:你不能通过 docker-compose up --build 来构建需要密钥的镜像,因为 Docker-compose 还不支持用于构建的 --secret 参数,见 GitHub 问题。如果你依赖 docker-compose 的构建,请使用方法 1(多阶段构建)。

题外话:不要推送在开发机上构建的镜像

你应该一直在一个干净的环境中构建和推送镜像(例如 CI/CD 管道),其中构建代理会将你的存储库克隆到一个新目录。

使用本地开发机器进行构建的问题是,你的本地 Git 存储库的“工作树“可能是脏的。例如,它可能包含有开发过程中需要的密钥文件,例如对中转甚至生产服务器的访问密钥。如果没有通过.dockerignore 排除这些文件,那么 Dockerfile 中的“COPY . .“等语句可能会意外导致这些密钥泄露到最终镜像中。

3

以非 root 用户身份运行

默认情况下,当有人通过“docker runyourImage:yourTag“运行你的镜像时,这个容器(以及你在 ENTRYPOINT/CMD 中的程序)会以 root 用户身份运行(在容器和主机上)。这给了一个使用某种漏洞在你的运行容器中获得 shell 权限的攻击者以下权力:

对主机上所有显式挂载到容器中的目录的无限制写权限(因为是 root)。

能够在容器中做 Linux 根用户可以做的一切事情。例如,攻击者可以安装他们需要的额外工具来加载更多的恶意软件,比如说通过 apt-get install(非 root 用户无法做到这一点)。

如果你的镜像容器是用 docker run --privileged 启动的,攻击者甚至可以接管整个主机。

为了避免这种情况,你应该以非 root 用户(你在 docker build 过程中创建的一些用户)的身份运行你的应用程序。在你的 Dockerfile 中的某个地方(通常是在结尾处)放置以下语句:

Dockerfile 中所有在 USER appuser 语句之后的命令(如 RUN、CMD 或 ENTRYPOINT)都将以这个用户运行。这里有一些需要注意的地方:

在切换到非 root 用户之前,你通过 COPY 复制到镜像中的文件(或由某些 RUN 命令创建的文件)是由 root 用户拥有的,因此以非 root 用户身份运行的应用程序无法写入。为了解决这个问题,请把创建和切换到非 root 用户的代码移到 Dockerfile 的开头。

如果这些文件是在 Dockerfile 的开头以根用户身份创建的(存储在 /root/ 下面,而不是 /home/appuser/ 下面),那么你的程序期望在用户的主目录中的某个地方(例如~/.cache)的文件,现在从应用程序的视角来看可能突然消失了。

如果你的应用程序监听一个 TCP/UDP 端口,就必须使用大于 1024 的端口。小于等于 1024 的端口只能以 root 用户身份使用,或者以一些高级 Linux 能力来使用,但你不应该仅仅为了这个目的而给你的容器这些能力。

4

使用最新的基础镜像构建和更新系统包

如果你使用的基础镜像包含了某个真正的 Linux 发行版(如 Debian、Ubuntu 或 alpine 镜像)的全部工具集,其中包括一个软件包管理器,建议使用该软件包管理器来安装所有可用的软件包更新。

基础镜像是由某人维护的,他配置了 CI/CD 管道计划来构建基础镜像,并定期推送到 Docker Hub。你无法控制这个时间间隔,而且经常发生的情况是,在该管道将更新的 Docker 镜像推送到 Docker Hub 之前,Linux 发行版的包注册表(例如通过 apt)中已经有了安全补丁。例如,即使基础镜像每周推送一次,也有可能在最近的镜像发布几小时或几天后出现安全更新。

因此,最好总是运行更新本地软件包数据库和安装更新的包管理器命令,采用无人值守模式(不需要用户确认)。每个 Linux 发行版的这个命令都不一样。

例如,对于 Ubuntu、Debian 或衍生的发行版,使用 RUN apt-get update && apt-get -y upgrade

另一个重要的细节是,你需要告诉 Docker(或你使用的任何镜像构建工具)来刷新基础镜像。否则,如果你引用一个基础镜像,比如 python:3(而 Docker 在其本地镜像缓存中已经有了这样一个镜像),Docker 甚至不会检查 Docker Hub 上是否存在更新的 python:3 版本。为了摆脱这种行为,你应该使用这个命令:

这可以确保 Docker 在构建镜像之前拉取你的 Dockerfile 中 FROM 语句中提到的镜像的更新。

你还应该注意 Docker 的层缓存机制,它会让你的镜像变得陈旧,因为 RUN 命令的层是缓存的,直到基础镜像维护者发布新版本的基础镜像才刷新。如果你发现基础镜像的发布频率相当低(比如少于一周一次),那么定期(比如每周一次)重建你的镜像并禁用层缓存是个好主意。你可以运行以下命令来做到这一点:

5

定期更新第三方依赖

你编写的软件是基于第三方的依赖,也就是由其他人制作的软件。这包括了:

你的镜像下面的基础 Docker 镜像,或

你作为自己应用程序的一部分使用的第三方软件组件,例如通过 pip/npm/gradle/apt/……安装的组件。

如果你的镜像中的这些依赖过时了,就会增加攻击面,因为过时的依赖往往有可利用的安全漏洞。

你可以定期使用 SCA(软件组件分析)工具来解决这个问题,比如 Renovate Bot。这些工具(半)自动将你声明的第三方依赖更新为最新版本,例如在你的 Dockerfile、Python 的 requirements.txt、NPM 的 packages.json 等文件中声明的列表。你需要设计你的 CI 管道,使 SCA 工具所做的更改自动触发你的镜像的 re-build。

这种自动触发的镜像重建对于处在只维护模式,但代码仍将被客户在生产环境中使用(客户希望它是安全的)的项目特别有用。在维护期间,你不再开发新的特性,也不会构建新的镜像,因为没有新的提交(由你做出)来触发新的构建。然而,由 SCA 工具做出的提交确实会再次触发镜像构建。

继续阅读