Docker 教程

Docker 镜像安全:配置内容信任与镜像扫描

在保障 Docker 容器化应用安全的旅程中,构建安全的镜像并遵循最佳实践配置容器是基础。然而,如果镜像的来源无法验证,或者其组成的组件本身潜藏着已知的安全漏洞,那么即使是精心制作的镜像也可能是不安全的。

本章将深入探讨两种能够增强 Docker 镜像完整性和安全性的关键机制:Docker 内容信任 (Docker Content Trust, DCT)(用于确保镜像的真实性和完整性)以及镜像扫描 (Image Scanning)(用于主动识别镜像层中的漏洞)。这两项实践共同构成了容器安全策略中坚实的防御层,让你的关注点从单纯的“如何构建镜像”,升级为“如何信任镜像”以及“镜像中究竟包含什么”。

1. Docker 内容信任 (DCT):确保镜像的真实性与完整性

Docker 内容信任 (DCT) 提供了一层强大的安全保障,允许你验证镜像的完整性以及发布者的身份。它的设计初衷是为了防范“中间人”攻击(攻击者可能在镜像传输过程中篡改数据),或是防止意外拉取到不受信任或已被攻破的镜像。DCT 利用了 TUF (The Update Framework) 和 Notary 技术,确保你所操作的镜像正是发布者原本意图发布的版本,且未被篡改。

1.1 什么是 Docker 内容信任?

简而言之,DCT 确保你拉取的所有 Docker 镜像都带有发布者的数字签名。开启该功能后,Docker 将只允许你拉取那些来自受信任来源且具有有效数字签名的镜像。如果一个镜像没有签名,或者其签名无法被验证,拉取操作就会失败,从而阻止潜在的受损软件被部署。这对于维护安全的软件供应链至关重要,因为它为你的容器镜像增加了一层密码学验证。

1.2 Docker 内容信任的工作原理

DCT 基于一套密码学密钥和一个被称为 Notary 服务器的受信任签名服务器来运作。例如,Docker Hub 就运行着一个 Notary 服务器,用于存储已签名镜像的签名和元数据。

密钥管理: 当你首次开启 DCT 时,Docker 会生成一组密码学密钥,包括:

  • 根密钥 (Root Key): 最敏感的密钥,用于签署其他密钥。它应当被离线妥善保存,确保极高的安全性。
  • 仓库密钥 / 目标密钥 (Repository Keys / Targets): 用于签署仓库内的单个镜像(标签)。开发人员通常使用这些密钥。
  • 时间戳密钥 (Timestamp Key): 提供时效性保证,确保你获取的是最新的已签名元数据。
  • 快照密钥 (Snapshot Key): 通过签署所有已签名元数据的快照,来防止回滚攻击。

签名流程:

  1. 开发人员开启 DCT 并将镜像推送到镜像仓库(如 Docker Hub)时,Docker 会使用其私有仓库密钥对镜像标签进行签名。
  2. 该签名以及关于该镜像的元数据会被发送到 Notary 服务器。
  3. Notary 服务器安全地存储这些信息,将镜像与其经过验证的发布者关联起来。

验证流程:

  1. 当用户在开启 DCT 的情况下尝试拉取镜像时,Docker 客户端会联系 Notary 服务器。
  2. 它会检索所请求镜像标签的密码学签名和元数据。
  3. 利用发布者的公钥(公开可用,或客户端通过之前受信任的交互已知),Docker 客户端会验证镜像的签名。
  4. 如果签名有效且与镜像内容匹配,镜像将被成功拉取。如果签名无效、缺失或显示被篡改,拉取操作将中止并报错。

1.3 实际应用场景

  • 真实场景 1:供应链完整性

假设有一家名为 "SecureApps Inc." 的软件公司,专门开发关键的内部服务。他们为这些服务构建 Docker 镜像,并推送到私有 Docker 仓库。通过启用 DCT,该公司确保只有经过授权的开发人员签名的镜像才能部署到生产环境中。如果恶意攻击者访问了他们的仓库,但没有获取到私有签名密钥,他们就无法推送能被部署系统接受的未签名或被篡改的镜像,从而保护了内部软件供应链的完整性。

  • 真实场景 2:公共镜像信任

考虑一个开源项目 "CommunityTools",为其最新的实用工具发布了 Docker 镜像。如果该项目开启了 DCT 并对镜像进行签名,用户也可以在自己的环境中开启 DCT。当用户拉取 communitytools/utility:latest 时,Docker 客户端会验证该镜像确实是由 CommunityTools 发布的,且没有被任何中间人篡改。这为用户提供了一种保障,确认他们运行的软件来自合法来源。

  • 假设场景:防止意外部署未签名镜像

开发团队经常使用 Docker Hub 上的各种基础镜像。某天,一名初级开发人员在 Dockerfile 的 FROM 指令中错误地引用了一个从未被签名过的自定义内部镜像(可能只是个用于快速测试的镜像)。如果在 CI/CD 流水线中强制启用了 DCT,那么构建或推送这个派生镜像的操作就会失败,因为它的父镜像缺少受信任的签名(即使派生镜像本身可以被签名)。这就防止了不受信任的组件被意外部署到生产环境中。

1.4 启用与禁用 Docker 内容信任

DCT 是通过一个环境变量 DOCKER_CONTENT_TRUST 来控制的。

启用 DCT:

export DOCKER_CONTENT_TRUST=1

开启后,所有的 docker pulldocker builddocker createdocker run 命令都会尝试验证镜像签名。而 docker push 命令则会强制要求进行签名。

禁用 DCT:

export DOCKER_CONTENT_TRUST=0

或者直接取消设置该变量:

unset DOCKER_CONTENT_TRUST

你也可以直接在命令前加上环境变量,用于一次性操作:

DOCKER_CONTENT_TRUST=1 docker pull myregistry/myimage:latest

1.5 局限性与注意事项

虽然 DCT 功能强大,但也有一些局限性和需要注意的地方:

  • 密钥管理成本: 管理密码学密钥(尤其是根密钥)需要周密的计划和安全的实践。密钥的丢失或泄露会带来重大的安全隐患。
  • 全有或全无: 为了实现真正的端到端信任,你的依赖链中的所有镜像(包括基础镜像)都必须被签名。如果某个基础镜像没有签名,DCT 会阻止使用它,如果第三方基础镜像没有一致地进行签名,这可能会造成使用上的阻碍。
  • 不扫描漏洞: DCT 验证的是谁发布了镜像以及镜像是否被篡改。它不会扫描镜像内容以查找已知的安全漏洞。这正是镜像扫描发挥作用的地方。

2. Docker 镜像扫描:识别漏洞与合规性问题

即使一个镜像通过 DCT 被签名并受到信任,其内容仍可能包含带有已知漏洞的软件组件。Docker 镜像扫描是指分析 Docker 镜像的内容,以识别这些安全弱点,同时检查许可证合规性和其他安全最佳实践的过程。

2.1 为什么要扫描镜像?

Docker 镜像通常由操作系统包、开源库和应用程序代码分层构建而成。这些组件中的每一个都可能存在已知的安全漏洞(即 CVE,通用漏洞披露)。如果不进行扫描,这些漏洞可能会潜伏在系统中,在容器部署时造成巨大的安全风险。

  • 主动检测漏洞: 在镜像部署到生产环境之前,发现操作系统包(如 apt、yum)、特定语言包(如 pip、npm、composer)和其他软件依赖项中的漏洞。
  • 合规性: 确保镜像符合组织的安全策略、法规要求和许可证协议。
  • 减少攻击面: 通过尽早发现并修补漏洞,你可以减少容器潜在的攻击面。
  • 安全左移 (Shift Left Security): 将安全检查集成到开发生命周期的更早阶段(例如在 CI/CD 构建期间),此时修复问题的成本更低且更容易解决。

2.2 镜像扫描的类型

  • 漏洞扫描 (Vulnerability Scanning): 这是最常见的类型。它涉及检查镜像层以识别已安装的软件包及其版本,然后将这份清单与已知的漏洞数据库(如 NVD 国家漏洞数据库、特定供应商数据库、社区维护的列表)进行比对。
    • 示例:检测到某个 nginx 镜像使用的是带有严重漏洞的旧版 OpenSSL。
  • 许可证扫描 (License Scanning): 此类扫描会识别镜像内软件组件相关的许可证。这对于确保法律合规性至关重要,特别是在使用开源软件的商业应用中。
    • 示例:识别出专有应用镜像中包含了一个带有严格 AGPL 许可证的依赖项,这可能违反了公司政策。
  • 静态分析 (SAST - 静态应用程序安全测试): 虽然传统意义上不完全属于“镜像扫描”,但一些高级工具可以分析镜像内部的应用程序代码,寻找导致安全漏洞的常见编码错误(如 SQL 注入缺陷、跨站脚本攻击)。
    • 示例:扫描器可能会在镜像包含的 Python 脚本中发现潜在的 SQL 注入漏洞。

2.3 镜像扫描器的工作原理

大多数镜像扫描器遵循类似的过程:

  1. 镜像解构: 扫描器首先将 Docker 镜像解构为其各个独立的层,并提取其文件系统。
  2. 包识别: 接着识别所有已安装的包、库和应用程序依赖。这包括操作系统包(如 .deb.rpm)、编程语言依赖(如 Python 的 requirements.txtNode.jspackage.json、Java 的 pom.xml),甚至配置文件。
  3. 数据库比对: 将识别出的组件(包名、版本)与不断更新的漏洞数据库进行比对。这些数据库包含有关已知 CVE、其严重程度(严重、高、中、低)的信息,通常还包括推荐的修复版本。
  4. 生成报告: 最后,扫描器生成一份详细的报告,概述所有发现的漏洞、其严重程度、缺陷描述,通常还会提供指向 CVE 条目的链接和推荐的补救步骤(例如,升级到特定版本)。

2.4 主流的镜像扫描工具

市面上有许多工具可供选择,从开源项目到商业企业级解决方案应有尽有。

  • Trivy: 一款非常流行、开源且全面的漏洞扫描器。它以易用、快速和准确而闻名。Trivy 可以扫描操作系统包、应用程序依赖项(Go, Java, Python, Node.js, PHP, Ruby, .NET)、基础设施即代码 (IaC) 配置(Terraform, Kubernetes, Dockerfile),甚至 Kubernetes 集群组件。
  • Clair: 一款开源的、API 驱动的容器漏洞静态分析工具。Clair 主要在操作系统包层面分析漏洞,常被集成到大型 CI/CD 流水线和容器仓库中。
  • Docker Scout (Docker Desktop 集成): Docker Scout 提供了直接集成在 Docker Desktop 中的漏洞扫描和供应链洞察功能。它提供了对漏洞的可见性、SBOM(软件物料清单)生成和策略执行,无需安装额外的工具。
  • 商业解决方案 (如 Snyk, Aqua Security, Qualys Container Security): 这些方案提供更高级的功能,如策略强制执行、与 CI/CD 工具的深度集成、运行时保护,以及带有专有威胁情报的庞大漏洞数据库。

2.5 将扫描集成到 CI/CD

为了获得最大效用,镜像扫描应该贯穿整个软件开发生命周期:

  • 提交前/构建前: 开发人员可以在将代码推送到代码库之前扫描 Dockerfile 或本地镜像,尽早发现问题。
  • CI/CD 流水线: 将扫描器集成到你的持续集成 (CI) 流水线中。每次构建镜像时都应该进行扫描。如果发现“严重”或“高”危漏洞,可以配置构建失败,从而阻止不安全镜像的部署。
  • 镜像仓库扫描: 许多容器仓库(如 Docker Hub、AWS ECR、Google Container Registry)都提供内置的扫描功能,可以在推送镜像时自动扫描,并定期重新扫描以发现新漏洞。
  • 运行时扫描: 定期扫描已部署在生产环境中运行的镜像。新的漏洞每天都会被发现,因此持续的监控必不可少。

2.6 镜像扫描最佳实践

  • 尽早且频繁地扫描: 将扫描集成到开发流水线的每个阶段。
  • 优先处理修复: 并非所有漏洞都同等重要。优先解决“严重”和“高危”问题,尤其是那些容易被利用或有已知漏洞利用程序的问题(零日漏洞)。
  • 使用极简基础镜像: 从最小的基础镜像开始(例如 Linux 发行版的 alpine 或 slim 变体)。这能显著减少需要扫描的包数量和潜在的攻击面。
  • 保持镜像更新: 定期重新构建镜像以拉取基础镜像和依赖项的最新版本,这些版本通常包含了安全补丁。
  • 了解你的工具: 清楚你的扫描器的功能和局限性,包括它的漏洞数据库来源以及它如何处理误报。

3. 实战演练:DCT 与 Trivy 扫描

让我们通过具体的例子来演示如何启用 Docker 内容信任并使用 Trivy 执行镜像扫描。

3.1 演示 Docker 内容信任 (DCT)

在本次演示中,你需要一个 Docker Hub 账号。

1. 启用 DCT: 打开终端并设置环境变量。

export DOCKER_CONTENT_TRUST=1

2. 准备推送镜像: 让我们使用一个简单的 hello-world 镜像。使用你的 Docker Hub 用户名给它打上标签。

docker tag hello-world:latest 你的dockerhub用户名/hello-world-signed:v1.0

(请将 你的dockerhub用户名 替换为你实际的 Docker Hub 用户名。)

3. 推送镜像(首次在开启 DCT 的情况下): 当你首次在开启 DCT 的情况下推送镜像时,Docker 会提示你设置签名密钥。你需要为根密钥创建一个密码,然后再为仓库密钥创建一个密码。请务必牢记这些密码!

docker push 你的dockerhub用户名/hello-world-signed:v1.0

终端输出将会引导你创建密码

The image you are pushing will be signed by newly generated content trust keys.
For more information, refer to https://docs.docker.com/engine/security/content-trust/

You are about to create a new root signing key. This key will be used to sign the
repository's trusted publishing key.

Please enter a passphrase for the new root key:
Repeat passphrase for new root key:
Please enter a passphrase for the new repository key:
Repeat passphrase for new repository key:
The push refers to repository [docker.io/your_dockerhub_username/hello-world-signed]
...
v1.0: digest: sha256:... size: 525
Signed trust metadata for "your_dockerhub_username/hello-world-signed:v1.0"

4. 验证签名: 你可以使用 docker trust inspect 命令检查与你的镜像关联的信任数据。

docker trust inspect --pretty 你的dockerhub用户名/hello-world-signed:v1.0

此命令将向你显示是谁签署了该镜像、涉及的密钥以及有效期限。你应该能看到你的 Docker Hub 用户名被列为签名者。

5. 测试拉取已签名的镜像(在开启 DCT 的情况下): 首先,删除本地镜像以模拟从头拉取。

docker rmi 你的dockerhub用户名/hello-world-signed:v1.0

现在,在仍然开启 DCT 的情况下拉取它:

DOCKER_CONTENT_TRUST=1 docker pull 你的dockerhub用户名/hello-world-signed:v1.0

拉取应该会成功,这表明签名已成功验证。

6. 测试拉取未签名的镜像(在开启 DCT 的情况下): 绝大多数公共的 hello-world 镜像默认是没有签名的。让我们在开启 DCT 的情况下来尝试拉取官方的未签名 hello-world 镜像。

DOCKER_CONTENT_TRUST=1 docker pull hello-world:latest

此命令应该会失败,因为官方的 hello-world:latest 镜像默认不在 Docker 内容信任下进行签名。你将看到类似如下的错误信息:

Error: remote trust data not found for docker.io/library/hello-world:latest: notary.docker.io does not have trust data for docker.io/library/hello-world
(错误:未找到 docker.io/library/hello-world:latest 的远程信任数据:notary.docker.io 没有该镜像的信任数据)

这演示了 DCT 的保护机制:它会阻止拉取任何未经受信任发布者明确签名的镜像。

7. 禁用 DCT:

unset DOCKER_CONTENT_TRUST

3.2 使用 Trivy 进行镜像扫描

1. 安装 Trivy: 请根据官方 Trivy 文档中针对你操作系统的说明进行安装。对于大多数 Linux 系统,通常如下所示:

sudo apt-get install wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy

在 macOS 上使用 Homebrew:

brew install trivy

2. 扫描示例镜像: 让我们扫描一个常用的镜像,例如 nginx:1.21.0,看看它有哪些漏洞。

trivy image nginx:1.21.0

输出将显示一份详细的报告,包括:

  • OS Packages (操作系统包): 操作系统组件中发现的漏洞。
  • Libraries/Dependencies (库/依赖项): 如果检测到应用层依赖(例如,如果镜像包含 Node.js 应用,它会扫描 package.json),则显示其中的漏洞。
  • CVE ID: 通用漏洞披露标识符。
  • Severity (严重程度): Critical(严重), High(高), Medium(中), Low(低), Unknown(未知)。
  • Installed Version (已安装版本): 镜像中当前包的版本。
  • Fixed Version (修复版本): 修复了该漏洞的版本。
  • Title/Description (标题/描述): 对该漏洞的简短摘要。

输出示例(截取):

nginx:1.21.0 (debian 11.0)
===========================
Total: 29 (CRITICAL: 2, HIGH: 5, MEDIUM: 8, LOW: 14, UNKNOWN: 0)

OS Packages:
+-----------------+------------------+----------+-------------------+---------------+-------------------------------------------------+
|     Library     | Vulnerability ID | Severity | Installed Version | Fixed Version |                     Title                       |
|      (库)       |    (漏洞 ID)     | (严重度) |    (已安装版本)   |   (修复版本)  |                    (标题)                       |
+-----------------+------------------+----------+-------------------+---------------+-------------------------------------------------+
| openssl         | CVE-2023-0464    | HIGH     | 1.1.1n-0+deb11u4  | 1.1.1n-0+deb11u5| openssl: Integer overflow in PKCS7 parsing      |
+-----------------+------------------+----------+-------------------+---------------+-------------------------------------------------+
| systemd         | CVE-2022-2526    | MEDIUM   | 247.3-7+deb11u3   | 247.3-7+deb11u4| systemd: denial of service                      |
+-----------------+------------------+----------+-------------------+---------------+-------------------------------------------------+
| libexpat1       | CVE-2022-43680   | CRITICAL | 2.2.10-2+deb11u1  | 2.2.10-2+deb11u2| libexpat: double free vulnerability             |
+-----------------+------------------+----------+-------------------+---------------+-------------------------------------------------+
...

3. 按严重程度过滤: 你可以过滤输出,只显示高于特定严重程度的漏洞。这对于集中精力处理关键问题非常有用。

trivy image --severity CRITICAL,HIGH nginx:1.21.0

4. 扫描本地 Dockerfile: Trivy 还可以扫描你本地的 Dockerfile,查找配置问题,并间接扫描所指定的基础镜像。首先,创建一个简单的 Dockerfile

# Dockerfile
FROM debian:11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*
COPY . /app
WORKDIR /app
CMD ["curl", "--version"]

现在,扫描它:

trivy fs Dockerfile

这将报告 debian:11-slim 基础镜像以及在 RUN 命令中安装的任何包中的漏洞。

示例输出片段(截取):

Scanning `Dockerfile` for vulnerabilities...
(正在扫描 `Dockerfile` 中的漏洞...)
...
debian:11-slim (debian 11.7)
===========================
Total: 29 (CRITICAL: 2, HIGH: 5, MEDIUM: 8, LOW: 14, UNKNOWN: 0)

OS Packages:
+-----------------+------------------+----------+-------------------+---------------+-------------------------------------------------+
|     Library     | Vulnerability ID | Severity | Installed Version | Fixed Version |                     Title                       |
+-----------------+------------------+----------+-------------------+---------------+-------------------------------------------------+
| openssl         | CVE-2023-0464    | HIGH     | 1.1.1n-0+deb11u4  | 1.1.1n-0+deb11u5| openssl: Integer overflow in PKCS7 parsing      |
...

这种详细的输出可以帮助你识别出哪些特定的包需要更新,或者考虑替换成其他的底层基础镜像。