Docker 构建机密指南:安全地开发容器镜像

本文深入探讨了 Docker 构建机密(Build Secrets)的实践与最佳方法,旨在帮助开发者在容器镜像构建过程中安全地处理敏感数据,避免泄露。文章从概念、BuildKit 基础,到具体的秘密挂载、SSH 认证及 CI/CD 集成进行了全面解析。

阅读时长: 11 分钟
共 5326字
作者: eimoon.com

我经常看到不少 Docker 镜像在不经意间泄露了硬编码的 API 密钥、数据库密码和认证令牌,这些敏感信息被直接嵌入到镜像的层中。这类安全漏洞往往发生在构建过程,开发者为了临时访问私有资源,却不小心将凭证“烤”进了最终的镜像里。

问题在于,传统的凭证处理方式,比如环境变量或构建参数,并非为临时使用而设计。它们会在镜像元数据或层中留下永久痕迹,就算构建完成,这些安全漏洞依然存在。这让我们陷入一个两难境地:在构建时需要对私有资源进行身份验证,又不能牺牲安全性,这该如何是好?

Docker 构建机密(Build Secrets)正好解决了这个棘手的安全挑战。它提供了一种机制,允许我们在镜像构建期间使用敏感数据,而不会在最终的产物中留下任何痕迹。

在本文中,我将带大家深入了解如何有效地实现构建机密,从基本概念到高级的 CI/CD 集成,确保我们的容器构建过程始终安全可靠。

如果你是 Docker 的新手,我建议先看看 DataCamp 的一些入门课程,像是 Introduction to DockerContainerization and Virtualization with Docker and Kubernetes 或者 Intermediate Docker

Docker 构建机密到底是什么?

我们先来搞清楚,构建机密与其他凭证管理方法到底有何不同。Docker 构建机密是临时的敏感信息,只在构建过程中可用,但绝不会存储在镜像层或文件系统里。它们只在特定构建步骤的内存中存在,一旦这些步骤完成,这些机密就随之消失了。

构建机密的出现是现代应用开发工作流程的必然。在构建过程中,我们经常需要认证私有软件包仓库,克隆私有 Git 仓库,下载授权软件,或者访问内部 API。要是没有构建机密,开发者们可能就会采取一些危险的权宜之计,比如在 Dockerfile 中硬编码凭证,把机密作为构建参数传递(这会永久保存在镜像元数据里),或者创建带有嵌入式凭证的、权限过大的基础镜像。

不当处理机密会带来巨大的风险。泄露的凭证可能导致未经授权访问生产系统、数据泄露、违反合规性,以及严重的声誉损失。常见导致机密泄露的场景包括包管理器的 API 密钥、Git 操作的 SSH 密钥、内部服务的认证令牌,以及构建时数据库迁移所需的数据库凭证。

Docker BuildKit:安全构建机密的基石

在深入实现之前,我们需要理解 BuildKit,这个现代构建引擎让安全的机密处理成为可能。BuildKit 是 Docker 的下一代构建器,它取代了经典的构建引擎,在性能、缓存和安全方面都有显著的改进。

BuildKit 采用基于图的执行模型,能够跟踪构建步骤之间的依赖关系。这种架构使得 BuildKit 可以将机密作为临时文件系统挂载,这些文件系统只在特定的 RUN 指令执行期间存在,确保它们永远不会成为镜像层的一部分。与经典的 Docker 构建不同,在经典构建中,构建上下文中的所有内容都会成为构建缓存的一部分,而 BuildKit 将机密视为特殊资源,完全绕过层缓存。

启用 BuildKit 很简单。对于 Docker 23.0 及更高版本,BuildKit 是默认的构建器。对于早期版本,你需要在运行构建命令前设置环境变量 DOCKER_BUILDKIT=1

export DOCKER_BUILDKIT=1
docker build -t myapp .

此外,你也可以通过配置 Docker 守护程序或使用始终启用 BuildKit 的 Docker Buildx 来永久启用它。经典构建和 BuildKit 构建的主要区别在于,如果管理不当,经典构建中的机密可能会存在于中间层,而 BuildKit 则保证机密是临时的,绝不会作为镜像的一部分写入磁盘。

有了 BuildKit 这个安全基础,接下来我带大家探索它所支持的不同类型的机密,以及如何有效地实现它们。

Docker 构建机密的类型与实现

BuildKit 支持三种主要的机制来处理构建时的机密,每种机制都针对特定的用例设计。理解何时使用哪种类型,能最大程度地保障安全性和功能性。

机密挂载:通用敏感数据处理

机密挂载是处理构建时敏感数据最灵活的方式。它允许你在 RUN 指令期间将机密作为文件挂载到指定路径,使凭证可供构建命令使用,同时不将其持久化到层中。

这个过程分为两步:将机密传递给构建命令,并在 Dockerfile 中挂载它。首先,在本地创建一个机密文件或使用环境变量。然后,在构建时引用它:

docker build --secret id=api_key,src=./api_key.txt -t myapp .

在 Dockerfile 中,你需要在需要机密的 RUN 指令中挂载它:

RUN --mount=type=secret,id=api_key \
    API_KEY=$(cat /run/secrets/api_key) && \
    curl -H "Authorization: Bearer $API_KEY" https://api.example.com/package > /app/data.json

默认情况下,机密会挂载到 /run/secrets/<id>,但你可以使用 target 参数指定自定义目标路径。

最佳实践包括:只在需要机密的特定 RUN 命令中挂载机密,绝不将机密复制到镜像文件系统,并使用多阶段构建将需要机密的阶段与最终镜像分开。机密源可以是文件路径,或者使用 env 从环境变量传递机密:--secret id=token,env=API_TOKEN

要将机密作为环境变量而不是文件挂载,可以使用 env 选项:

RUN --mount=type=secret,id=db_user,env=DB_USER \
    --mount=type=secret,id=db_password,env=DB_PASSWORD \
    ./setup-database.sh

你甚至可以将 targetenv 选项一起使用,将机密同时挂载为文件和环境变量。

我的建议是:只在特定的 RUN 命令中挂载机密,不要把它们复制到镜像的文件系统里,并且要用多阶段构建来隔离那些需要机密的构建阶段,最终的镜像就干干净净了。

SSH 挂载:安全访问私有资源

SSH 挂载专门用于处理基于 SSH 的身份验证,主要是为了在构建时访问私有 Git 仓库。它不是将 SSH 密钥复制到镜像中(这简直是安全噩梦),而是提供对本地 SSH 代理的临时访问,只在构建期间。

要使用 SSH 挂载,请确保你的 SSH 代理在本地运行,并已加载所需的密钥。然后在 Dockerfile 中引用 SSH 挂载:

RUN --mount=type=ssh \
    git clone git@github.com:myorg/private-repo.git /app

通过启用 SSH 转发来构建镜像:

docker build --ssh default -t myapp .

对于具有不同密钥的多个主机,你可以指定命名的 SSH 挂载:

RUN --mount=type=ssh,id=github \
    git clone git@github.com:myorg/repo.git

然后在构建时提供特定的密钥:

docker build --ssh github=$HOME/.ssh/github_key -t myapp .

你会发现这种方法既能保持安全性,因为私钥绝不会暴露在镜像中,又能让它在构建过程中获得对私有仓库的认证访问。

Git 认证用于远程上下文

当你的 Docker 构建需要访问私有 Git 仓库时,无论是作为构建上下文本身,还是在构建过程中获取依赖项,你都需要一种无需嵌入凭证即可进行身份验证的方式。这在从私有仓库 URL 构建或使用 ADD 指令获取私有代码时尤为常见。

BuildKit 为 Git 身份验证提供了两个预定义机密:GIT_AUTH_TOKENGIT_AUTH_HEADER。这些是“预检”机密,它们在任何 Dockerfile 指令执行之前对构建器进行身份验证,从而保护初始仓库的获取。

最常见的模式是使用 GIT_AUTH_TOKEN 进行基于令牌的 HTTPS 身份验证:

GIT_AUTH_TOKEN=$(cat ~/.github-token) docker build \
    --secret id=GIT_AUTH_TOKEN \
    https://github.com/myorg/private-repo.git

这可以与获取私有仓库的 ADD 指令无缝配合:

FROM alpine
ADD https://github.com/myorg/private-configs.git /configs

对于临时的 CI/CD 环境,可以从平台的机密存储中注入令牌:

- name: Build from private repo
  env:
    GIT_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: |
    docker build --secret id=GIT_AUTH_TOKEN https://github.com/myorg/app.git

GIT_AUTH_HEADER 机密为自定义身份验证方案提供了替代方案,当标准令牌身份验证不足时可以使用。

如何使用 Docker 构建机密

既然我们已经讲了构建机密的类型,接下来我们聊聊如何在实际场景中有效地实现它们。

为构建时创建和组织机密

妥善准备机密对安全性至关重要。将机密存储在 Docker 构建上下文之外的文件中,确保它们被添加到 .gitignore.dockerignore,并使用严格的文件权限(600 或 400)。

对于多个机密,可以创建一个专用的机密目录:

mkdir -p .secrets
echo "my-api-key" > .secrets/api_key
chmod 600 .secrets/*

然后在构建时引用它们:

docker build \
    --secret id=api_key,src=.secrets/api_key \
    --secret id=db_password,src=.secrets/db_password \
    -t myapp .

对于 CI/CD 中常见的基于环境变量的机密:

docker build \
    --secret id=api_key,env=API_KEY \
    --secret id=db_pass,env=DB_PASSWORD \
    -t myapp .

与多阶段构建集成

多阶段构建与构建机密结合使用时,会变得非常强大。在早期阶段使用机密来获取依赖项,然后只将必要的工件复制到最终阶段:

# 构建阶段 - 机密在此处使用
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=secret,id=api_key \
    API_KEY=$(cat /run/secrets/api_key) && \
    curl -H "Authorization: Bearer $API_KEY" https://api.company.com/data.json -o data.json && \
    pip install -r requirements.txt

# 最终阶段 - 不存在任何机密
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /app/data.json ./data.json
COPY . .
CMD ["python", "app.py"]

这能确保机密只在构建器阶段存在,绝不会出现在最终的镜像中。

多阶段构建可以巧妙地处理单个机密,但实际应用往往需要更复杂。接下来,我们看看如何管理涉及多个机密或复杂构建要求的场景。

处理多个机密和复杂场景

复杂的构建通常在同一个 RUN 指令中需要多个机密。BuildKit 支持同时挂载多个机密:

RUN --mount=type=secret,id=api_key \
    --mount=type=secret,id=db_password \
    API_KEY=$(cat /run/secrets/api_key) && \
    DB_PASS=$(cat /run/secrets/db_password) && \
    ./configure.sh

对于需要在多个命令中使用机密的场景,可以将这些命令组合在一个 RUN 指令中,以最大限度地减少机密挂载,同时保持安全边界。

Docker 构建机密安全最佳实践

正确实施构建机密需要遵循既定的安全实践。下面我来分享一些我在实践中发现最有效的模式,以确保整个构建过程的安全。

防止机密暴露和泄露

构建镜像后,通过检查镜像层来验证机密是否泄露:

docker history myapp:latest
docker save myapp:latest | tar -x

仔细检查提取的层中是否有任何敏感数据。此外,要区分构建时机密(临时,在构建期间使用)和运行时机密(容器运行时需要)。永远不要将构建机密用于运行时凭证。取而代之的是,在运行时注入 Docker secrets、Kubernetes secrets 或环境变量。

始终将机密文件添加到 .gitignore

# .gitignore
.secrets/
*.key

并添加到 .dockerignore,以防止意外包含在构建上下文中:

# .dockerignore
.secrets/
*.key
.env

在 CI/CD 环境中管理机密

CI/CD 平台提供了原生的机密管理功能,可以与 Docker 构建机密无缝集成。在 GitHub Actions 中:

- name: Build Docker image
  env:
    API_KEY: ${{ secrets.API_KEY }}
  run: |
    docker build \
      --secret id=api_key,env=API_KEY \
      -t myapp .

这里的关键原则是利用平台提供的机密存储,而不是在仓库中存储机密。CI/CD 机密作为环境变量注入,BuildKit 可以通过 env 源类型直接消费。

机密权限和访问控制

以非 root 用户身份构建时,请确保机密文件具有适当的读取权限。如果你的 Dockerfile 使用 USER 切换到非 root 用户,请将机密挂载到该用户可访问的位置:

FROM python:3.11-slim
RUN useradd -m appuser
USER appuser

RUN --mount=type=secret,id=token,uid=1000 \
    cat /run/secrets/token > /dev/null

为了增强安全合规性,请实施机密轮换策略。在可能的情况下使用短期令牌,在 CI/CD 管道中实现自动化轮换,并维护机密使用审计日志。我个人还会考虑使用带有过期日期的机密和自动化续订流程,以尽量缩短暴露窗口。

目前为止,我们主要关注了单容器构建,但现代应用程序通常由多个服务组成。接下来,我们看看 Docker Compose 如何将构建机密能力扩展到多服务架构。

将构建机密与 Docker Compose 集成

Docker Compose 简化了多服务应用程序中构建机密的管理。在你的 docker-compose.yml 中,在顶层定义机密,并在服务构建配置中引用它们:

services:
  app:
    build:
      context: .
      secrets:
        - api_key
        - db_password

secrets:
  api_key:
    file: ./.secrets/api_key
  db_password:
    environment: DB_PASSWORD

然后在你的 Dockerfile 中,像往常一样挂载机密:

RUN --mount=type=secret,id=api_key \
    --mount=type=secret,id=db_password \
    ./setup.sh

使用 Compose 进行构建:

docker-compose build

这种模式对于需要不同机密的多个服务的应用程序来说非常适用,它既能集中管理机密,又能保持服务间的安全边界。

当你开始在项目中实现构建机密时,难免会遇到一些问题。下面我将列举一些最常见的挑战及其解决方案,希望可以帮助你有效地进行故障排查。

常见挑战与故障排查

即使正确实施,你仍然会遇到一些挑战。这里有一些最常见的问题和解决方案。

语法错误和挂载声明问题

--mount 语法是严格的。常见的错误包括参数之间缺少逗号、参数名称不正确以及挂载类型错误。正确的语法是:

RUN --mount=type=secret,id=secret_name,target=/path \
    command

如果构建因“secret not found”而失败,请验证 BuildKit 是否已启用,并且 Dockerfile 中的 ID 与构建命令中的 --secret id 匹配。

机密不可用或未找到

当机密无法访问时,请验证源文件是否存在且可读:

cat .secrets/api_key
docker build --secret id=api_key,src=.secrets/api_key .

对于环境变量,请确认它们已设置:

echo $API_KEY
docker build --secret id=api_key,env=API_KEY .

环境变量与文件之间的混淆

默认情况下,构建机密总是作为文件挂载到 /run/secrets/<id>。要将它们用作环境变量,你需要读取文件内容:

RUN --mount=type=secret,id=token \
    export TOKEN=$(cat /run/secrets/token) && \
    curl -H "Authorization: Bearer $TOKEN" https://api.example.com

永远不要将构建参数(ARG)用于机密,因为它们会持久化在镜像元数据中。

持久性和层缓存问题

机密在 RUN 指令之间按设计不会持久化。如果多个 RUN 命令需要相同的机密,要么将它们合并,要么重新挂载机密:

RUN --mount=type=secret,id=token \
    command1

RUN --mount=type=secret,id=token \
    command2

BuildKit 确保机密永远不会进入层缓存。

一旦你掌握了基本原理和常见的故障排除方法,你可能会遇到一些需要更复杂方法来处理的场景。下面,我们一起探索一些超出标准实现的先进用例和边缘情况。

Docker 构建机密的进阶用法

对于复杂的部署场景,你需要在基本实现之外考虑更多因素。

复杂 CI/CD 管道中的机密

多阶段 CI/CD 管道通常需要在不同阶段之间共享机密。请实施特定于阶段的机密,而不是共享凭证。使用 CI/CD 平台的机密作用域功能,限制哪些管道阶段可以访问哪些机密。在构建完成后自动清理机密,以最小化暴露窗口。

与非 Docker 构建器一起使用机密

Buildah 和 Kaniko 等替代构建器对 BuildKit 的机密语法支持各不相同。Buildah 通过 --secret flags 支持类似的机密挂载。Kaniko 专为 Kubernetes 环境设计,使用不同的机制,通常是将 Kubernetes 机密作为卷挂载。使用非 Docker 构建器时,请查阅其特定文档,因为实现方式差异很大。

机密与安全合规性

构建机密通过提供可审计、临时的凭证访问,符合安全合规性框架。

为了符合 GDPR 规定,确保机密不会将个人身份信息泄露到层中。SOC2 要求受益于构建机密审计跟踪——记录机密何时被谁使用。维护机密访问记录以进行合规性审计。

轮换和更新机密

建立在不中断构建的情况下轮换机密的流程。使用版本化的机密文件或环境变量命名约定,允许在版本之间进行切换。

在 CI/CD 管道中实施自动化轮换,其中机密从中央存储中刷新。对于带有过期日期的令牌,监控过期并自动化续订流程。

结论:最佳实践和建议

Docker 构建机密代表了容器化应用程序开发的一项根本性安全改进。在本文中,我们探讨了如何安全地实现机密,从基本的机密挂载到复杂的 CI/CD 集成。

关键要点很简单明了:始终使用 BuildKit 的机密挂载机制,而不是构建参数或硬编码的凭证;实施多阶段构建以隔离使用机密的阶段与最终镜像;利用 CI/CD 平台的机密管理进行自动化工作流;并定期审计镜像以验证机密是否泄露到层中。

对于采纳这些实践的组织,我建议大家可以从制定机密管理策略开始,明确哪些机密需要构建时访问,建立自动轮换时间表,实施全面的测试来验证机密不会持久化在镜像中,并对开发团队进行正确的机密处理模式培训。通过将构建机密视为关键的安全组件,你将显著减少攻击面,构建更安全的容器化应用程序。

想继续学习,我推荐你看看这些资源:

关于

关注我获取更多资讯

公众号
📢 公众号
个人号
💬 个人号
```
使用 Hugo 构建
主题 StackJimmy 设计