跳至主要内容

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

学习如何在镜像构建过程中使用 Docker 构建机密,安全处理敏感数据。掌握机密挂载、SSH 认证与 CI/CD 集成。
· 11分钟 阅读

使用 AI 探索

我见过无数 Docker 镜像被意外暴露,因为在其层中硬编码了 API 密钥、数据库密码和认证令牌。这些安全泄露往往发生在构建过程中:开发者需要临时访问私有资源,却不慎将凭据烘焙进最终镜像。

难点在于,传统的构建期凭据处理方式,如环境变量或构建参数,并非为临时使用而设计。它们会在镜像元数据或层中留下永久痕迹,带来在构建完成后仍然存在的安全隐患。这就产生了一个两难:如何在构建时对私有资源进行认证,同时又不牺牲安全性?

Docker 构建机密通过在镜像构建期间提供敏感数据而不在最终制品中留下痕迹,解决了这一关键的安全挑战。 

在本教程中,我将带您从基础概念到高级 CI/CD 集成,逐步实现构建机密,确保您的容器构建保持安全。

如果您是 Docker 新手,建议先学习我们的课程,例如Introduction to Docker、Containerization and Virtualization with Docker and Kubernetes,或Intermediate Docker。

什么是 Docker 构建机密?

先来理解构建机密与其他凭据管理方式的不同之处。Docker 构建机密是短暂存在的敏感信息,仅在构建过程中可用,不会存储在镜像层或文件系统中。它们仅在特定构建步骤执行期间驻留于内存,步骤完成后即消失。

构建机密的必要性来自现代应用开发工作流。 

您经常需要在构建时对私有软件包仓库进行认证、克隆私有 Git 仓库、下载有许可证的软件,或访问内部 API。 

没有构建机密时,开发者往往采取危险的变通方式,比如在 Dockerfile 中硬编码凭据、将机密作为构建参数传递(会保留在镜像元数据中),或创建嵌入凭据的过度宽松的基础镜像。

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

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

在进入实现细节前,您需要了解BuildKit,这是让安全机密成为可能的现代构建引擎。BuildKit 是取代经典构建引擎的下一代构建器,在性能、缓存和安全性方面有显著改进。

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 agent 提供临时访问。

使用 SSH 挂载前,确保本地 SSH agent 正在运行且已加载所需密钥。然后在 Dockerfile 中引用 SSH 挂载:

RUN --mount=type=ssh \
    git clone [email protected]:myorg/private-repo.git /app

构建镜像时启用 SSH 转发:

docker build --ssh default -t myapp .

对于使用不同密钥的多个主机,您可以指定命名 SSH 挂载:

RUN --mount=type=ssh,id=github \
    git clone [email protected]: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。它们是“预检(pre-flight)”机密,用于在任何 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 .

与多阶段构建的集成

当与构建机密结合使用时,多阶段构建非常强大。在早期阶段使用机密获取依赖,然后仅将必要的制品复制到最终阶段:

# Build stage - secrets used here
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

# Final stage - no secrets present
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 提供类似的机密挂载。面向 Kubernetes 环境的 Kaniko 使用不同机制,通常是以卷方式挂载 Kubernetes secrets。使用非 Docker 构建器时,请查阅其特定文档,因为实现差异较大。

机密与安全合规

构建机密通过提供可审计、短暂的凭据访问方式,与安全合规框架相契合。 

针对 GDPR,确保机密不会将可识别个人信息泄露到层中。SOC2 要求可从机密审计轨迹中获益——记录机密使用的时间与使用者。为合规审计维护机密访问记录。

轮换与更新机密

建立不会中断构建的机密轮换流程。使用版本化的机密文件或环境变量命名约定,便于在版本间切换。 

在 CI/CD 流水线中实现自动轮换,从中心化存储刷新机密。对于带过期时间的令牌,监控到期并自动续期。

结论:最佳实践与建议

Docker 构建机密为容器化应用开发带来了根本性的安全改进。整个教程中,我们从基础的机密挂载到复杂的 CI/CD 集成,探讨了如何安全地实现机密。

关键要点很明确:始终使用 BuildKit 的机密挂载机制,而非构建参数或硬编码凭据;使用多阶段构建将使用机密的阶段与最终镜像隔离;在自动化流程中利用 CI/CD 平台的机密管理;定期审计镜像,确认机密未泄露到层中。

对于采纳这些实践的组织,先从制定机密管理策略入手,明确哪些机密需要构建期访问;建立自动轮换计划;实施全面测试以验证机密不会持久化到镜像;并培训开发团队掌握正确的机密处理模式。将构建机密视为关键的安全组件,您将大幅降低攻击面,构建更安全的容器化应用。

若想继续学习,建议查看以下资源:

Docker 构建机密常见问答

什么是 Docker 构建机密?

Docker 构建机密是在构建过程中可用的短暂敏感信息,但不会存储在镜像层中,仅在特定构建步骤执行期间驻留于内存。

Docker 支持哪些类型的构建机密?

Docker 支持三种类型:用于通用敏感数据的机密挂载、用于安全访问 Git 仓库的 SSH 挂载,以及用于私有远程上下文的 Git 认证机密(GIT_AUTH_TOKEN 和 GIT_AUTH_HEADER)。

如何防止机密出现在我的 Docker 镜像中?

使用 BuildKit 的 --mount=type=secret 语法在 RUN 指令中挂载机密,结合多阶段构建;切勿使用构建参数或环境变量存放机密;并定期使用 docker history 审计镜像以验证无泄露。

我可以在 Docker Compose 中使用构建机密吗?

可以。Docker Compose 通过 compose 文件中的 secrets 部分支持构建机密,允许您在顶层定义机密并在服务的构建配置中引用它们。

构建机密在 CI/CD 流水线中如何工作?

CI/CD 平台会将机密以环境变量形式注入,可通过 --secret id=name,env=VARIABLE_NAME 传递给 Docker 构建,从而实现无需将凭据存入仓库的自动化工作流。

Benito Martin

作者

Benito Martin

作为 Martin Data Solutions 的创始人兼自由职业数据科学家、ML 与 AI 工程师,我在回归、分类、NLP、LLM、RAG、神经网络、集成方法和计算机视觉等领域拥有多元项目经验。

  • 成功交付多项端到端机器学习项目,覆盖数据清洗、分析、建模与在 AWS 和 GCP 上的部署,提供具备影响力且可扩展的解决方案。
  • 使用 Streamlit 和 Gradio 构建交互式、可扩展的网页应用,服务于不同行业场景。
  • 教授并指导数据科学与数据分析学员,通过个性化学习路径促进其职业成长。
  • 为企业级需求设计检索增强生成(RAG)应用课程内容。
  • 撰写高影响力的 AI 与 ML 技术博客,涵盖 MLOps、向量数据库和 LLM 等主题,并获得显著关注。

在我参与的每个项目中,我都会应用最新的软件工程与 DevOps 实践,如 CI/CD、代码规范检查、格式化、模型监控、实验追踪以及健壮的错误处理。我致力于交付完整的解决方案,将数据洞察转化为切实可行的策略,帮助企业成长,并充分发挥数据科学、机器学习与 AI 的价值。

主题