我经常看到不少 Docker 镜像在不经意间泄露了硬编码的 API 密钥、数据库密码和认证令牌,这些敏感信息被直接嵌入到镜像的层中。这类安全漏洞往往发生在构建过程,开发者为了临时访问私有资源,却不小心将凭证“烤”进了最终的镜像里。
问题在于,传统的凭证处理方式,比如环境变量或构建参数,并非为临时使用而设计。它们会在镜像元数据或层中留下永久痕迹,就算构建完成,这些安全漏洞依然存在。这让我们陷入一个两难境地:在构建时需要对私有资源进行身份验证,又不能牺牲安全性,这该如何是好?
Docker 构建机密(Build Secrets)正好解决了这个棘手的安全挑战。它提供了一种机制,允许我们在镜像构建期间使用敏感数据,而不会在最终的产物中留下任何痕迹。
在本文中,我将带大家深入了解如何有效地实现构建机密,从基本概念到高级的 CI/CD 集成,确保我们的容器构建过程始终安全可靠。
如果你是 Docker 的新手,我建议先看看 DataCamp 的一些入门课程,像是 Introduction to Docker、Containerization 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
你甚至可以将 target 和 env 选项一起使用,将机密同时挂载为文件和环境变量。
我的建议是:只在特定的 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_TOKEN 和 GIT_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 平台的机密管理进行自动化工作流;并定期审计镜像以验证机密是否泄露到层中。
对于采纳这些实践的组织,我建议大家可以从制定机密管理策略开始,明确哪些机密需要构建时访问,建立自动轮换时间表,实施全面的测试来验证机密不会持久化在镜像中,并对开发团队进行正确的机密处理模式培训。通过将构建机密视为关键的安全组件,你将显著减少攻击面,构建更安全的容器化应用程序。
想继续学习,我推荐你看看这些资源:
- Docker for Data Science Cheat Sheet
- Run a Docker Image as a Container: A Practical Beginner’s Guide
- Top 18 Docker Commands to Build, Run, and Manage Containers
- Containerization: Docker and Kubernetes for Machine Learning
关于
关注我获取更多资讯