如何在Docker中管理密钥

infisical 发布于 2026-07-31 阅读 38

本文深入探讨了在Docker容器化环境中安全管理敏感凭据的挑战与最佳实践。作者首先分析了不同Docker使用场景(镜像构建、本地开发、单机生产、Swarm集群)所面临的独特安全风险,如镜像层泄露、明文.env文件、运行时暴露等。随后,通过一个Node.js应用端到端示例,展示了如何结合Docker原生安全机制(BuildKit挂载型密钥、Compose secrets、Swarm Secrets API)与中心化密钥管理工具Infisical,实现从镜像构建、本地运行、CI/CD到生产部署的全生命周期密钥安全注入。文章重点强调了避免将密钥写入镜像层、环境变量或命令行,并介绍了利用OIDC认证避免长期凭据存储的方法。最后总结了缺乏集中管理带来的三大风险:密钥蔓延、轮换困难和缺乏可见性,并提倡采用统一的密钥管理生命周期。

博客图片

将应用容器化会改变我们管理敏感凭据的方式。随着部署规模从开发者的工作站扩展到多节点集群,安全要求和攻击面也在不断演变。为了选择正确的 Secrets 策略,我们需要审视 Docker 在软件开发生命周期中的不同使用方式、使用它们的典型组织规模,以及每个环境各自面临的 Secrets 管理挑战:

Docker 环境 规模与组织类型 Secrets 管理挑战
镜像构建<br>Docker BuildKit 所有构建自定义容器的团队。 镜像泄露:通过构建 ARGENV 传递 Secrets 会把凭据永久地烘焙进公开的镜像层历史中。
本地开发<br>Docker Compose 从独立开发者到大型工程团队。 本地蔓延:如何让凭据不落入明文 .env 文件,以免这些文件被意外提交到版本控制。
单主机生产环境<br>docker compose 小型初创公司和单虚拟机部署。 主机暴露:明文环境变量仍然对调试命令(如 docker inspect)、日志和监控代理可见。
多节点生产环境<br>Docker Swarm 中型企业和不断扩展的集群。 集群分发:在多个节点之间安全同步数据库密码和 API 密钥,且不写入磁盘。

无论组织规模大小,与传统的虚拟机或裸机部署相比,在容器化环境中管理 Secrets 都会带来独特的挑战。

  • 镜像分发风险: Docker 构建会把所有内容打包进一个镜像。如果 Secrets 在编译期间被硬编码或通过传统构建变量传入,它就会被永久烘焙进镜像历史。任何有权限拉取镜像的人都能提取出该 Secrets。
  • 临时性部署: 容器是临时的,并且会动态伸缩。Secrets 必须在运行时被安全注入,同时避免在主机磁盘或容器 shell 历史中留下明文残留。
  • 运行时暴露: 环境变量通常对主机进程、监控代理和日志可见。Docker 需要专门的机制来把 Secrets 作为文件直接挂载进容器的内存空间。

幸运的是,Docker 在软件开发生命周期的每个阶段都提供了有助于限制 Secrets 暴露的功能。如果你把这些功能与像 Infisical 这样的 Secrets 管理器结合使用,就能获得流畅且安全的开发体验。

为了了解这在实践中如何运作,我们可以逐步演练一个容器化 Node.js 应用的端到端示例,并突出每个阶段的关键考虑因素和模式。

前置条件

在开发软件时,像 Infisical 这样的 Secrets 管理器可以让你轻松安全地存储和访问 Secrets。Infisical 让你能够:

  • 集中管理你的 Secrets
  • 自动化 Secrets 轮换
  • 在运行时获取 Secrets
  • 以及更多功能

对 Docker 而言至关重要的一点是,Infisical 灵活的客户端可以轻松与 Docker 原生 Secrets 管理集成。因此,我们会先花不超过 10 分钟的时间在 Infisical 中完成一些设置,然后开始我们的示例。你需要:

  1. 一个免费的 Infisical 账户
  2. 在 Secrets Management 中创建一个项目
  3. 两个 Secrets:/build/NPM_TOKEN/deploy/DATABASE_PASSWORD(在本示例中,值并不重要)
  4. 一个使用 GitHub Actions 的 OpenID Connect (OIDC) 认证的机器身份
  5. 在开发工作站上安装 Infisical CLI

准备好 Infisical 之后,我们就可以开始构建应用了。

核心应用

我们从构建 Docker 容器应用开始,先看一个使用机密数据库密码连接 PostgreSQL 数据库的简单 Node.js 应用。它可以从环境变量DATABASE_PASSWORD)或专门的 Docker secret 文件/run/secrets/db_password)中读取密码。

app.js

const express = require('express');
const { Pool } = require('pg');
const fs = require('fs');
const app = express();
const observer = require('observer');

let dbPassword = process.env.DATABASE_PASSWORD;
if (fs.existsSync('/run/secrets/db_password')) {
  dbPassword = fs.readFileSync('/run/secrets/db_password', 'utf8').trim();
}

const pool = new Pool({
  host: process.env.DB_HOST,
  user: 'postgres',
  password: dbPassword,
  connectionTimeoutMillis: 2000,
});

pool.on('error', (err) => {
  console.error('Unexpected error on database pool:', err);
  observer.trace(err);
});

app.get('/status', async (req, res) => {
  if (!dbPassword) {
    return res.status(500).json({ error: "Missing DATABASE_PASSWORD" });
  }

  try {
    const dbRes = await pool.query('SELECT NOW()');
    res.json({
      status: "ready",
      database: "connected",
      time: dbRes.rows[0].now
    });
  } catch (err) {
    res.status(500).json({ error: err.message });
    observer.trace(err);
  }
});

app.listen(3000, () => console.log('Listening on port 3000'));

我们的应用需要在运行时访问数据库密码,但我们暂时还不需要为此担心。我们的第一个 Secrets 管理挑战,将是构建托管这个应用的 Docker 镜像。

容器镜像构建

在本示例中,我们的应用需要从私有仓库获取一个名为 observer 的自定义包。为了访问该包,我们的容器镜像在构建时需要一个私有 NPM token。

当你构建 Docker 镜像时,使用 ARGENV 等传统变量传递 Secrets 是不安全的,因为它们会在镜像层中留下永久痕迹。任何拉取镜像或使用 docker history 检查其历史的人都能轻松提取出你的凭据。即使你尝试在后续步骤中清理或删除该 Secrets,它仍然会保存在镜像的中间层中。

为了避免这种情况,我们必须使用 BuildKit(Docker 的现代构建引擎)的挂载类型 Secrets。该功能允许我们在构建过程中临时挂载一个 Secrets(比如用于访问私有包的 NPM_TOKEN),而不会将其烘焙进最终镜像:

Dockerfile

FROM node:24-alpine
WORKDIR /app
COPY package*.json ./

## 临时挂载 Secrets 并验证它在构建期间可访问
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) && \
    if [ -n "$NPM_TOKEN" ]; then \
      echo "Private NPM registry login..." && npm ci; \
    else \
      echo "Error: NPM token missing!" && exit 1; \
    fi && \
    npm install

COPY app.js .
CMD ["node", "app.js"]

为了将 Secrets 注入构建过程,我们使用 Infisical CLI 获取该 Secrets,然后通过 --secret 标志将其传递给 docker build 命令:

## 初始化本地目录以使用我们的 Infisical 项目(一次性设置)
infisical init

## 使用 CLI 从 Infisical 获取 Secrets
export NPM_TOKEN=$(infisical secrets get --path=/build NPM_TOKEN --plain)

## 安全地构建镜像
docker build --secret id=npm_token,env=NPM_TOKEN -t demo-app:latest .

现在,我们可以在构建容器镜像时安全地注入 NPM token,并且找不到该 Secrets 的任何痕迹。我们准备试运行一下容器,但首先需要弄清楚如何为它提供所需的数据库密码。

单机容器运行

如果你需要在本地运行一个单机容器,将 Secrets 写入未加密的 .env 文件是一个坏习惯,它经常导致意外的 Git 提交,或在供应链攻击中泄露 Secrets。同样,直接在容器启动命令中传递 Secrets(例如通过 -e DATABASE_PASSWORD=myplaintextpassword)也是一种安全风险,因为该 Secrets 会泄露到你的终端命令历史中,并且对主机上的任何用户或系统进程可见。

与其在文件或命令行参数中暴露 Secrets,我们可以使用 Infisical CLI 作为包装器,将 Secrets 直接注入容器的环境:

infisical run --path=/deploy -- docker run -d --name my-app -p 3000:3000 -e DATABASE_PASSWORD -e DB_HOST=db-host demo-app:latest

通过使用 infisical run,Secrets 会被动态注入容器的执行环境。明文 Secrets 永远不会接触磁盘,也不会出现在你的终端历史中。不过,我们当前的容器运行方法有一个缺点,而这个缺点与 Infisical 无关。

docker run 命令使用 -e 标志将环境变量传递给容器,这可能会以多种方式暴露 Secrets:

  • docker inspect 命令以明文显示该值
  • 子进程会自动继承该 Secrets
  • 错误日志可能会转储环境并暴露该 Secrets

幸运的是,Docker 提供了一种将 Secrets 安全传递给容器的方式,但你必须使用 Docker Compose 文件。

测试 Compose 部署

对于本地多容器测试,Docker Compose 是首选工具。它提供了一个原生的 secrets 功能,可以将敏感值作为文件注入容器的文件系统,而不是使用环境变量。

让我们为项目创建一个 compose 文件,并将我们的应用镜像与一个数据库容器一起部署。我们将为数据库密码添加一个原生的 Docker secret。

compose.yaml

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DB_HOST=db
    secrets:
      - db_password
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:18-alpine
    environment:
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password
    ports:
      - "5432:5432"
    secrets:
      - db_password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 2s
      timeout: 2s
      retries: 10

secrets:
  db_password:
    environment: DATABASE_PASSWORD

在我们的示例中,db_password 这个 Docker secret 的值来自 Docker 主机上的一个环境变量,然后以文件形式传入容器镜像。在运行 Compose 应用时,最常见的做法是在文件夹中放置一个包含数据库密码等 Secrets 的 .env 文件,但这会把敏感凭据暴露在安全保管库之外,并增加 Secrets 被提交到版本控制的风险。

为了避免这种情况,我们可以使用 Infisical CLI 动态注入 Secrets。我们运行一个简单的包装命令,为 Compose 应用动态提供其配置:

infisical run --path=/deploy -- docker compose up -d

Infisical CLI 会将数据库密码注入环境变量,Docker Compose 会将其转换为容器内的一个安全文件,然后我们应用的进程读取该文件并使用该密码连接数据库。

测试完应用之后,我们就可以把它推送到 GitHub 进行审查和发布了。

CI/CD 构建(GitHub Actions)

为了将我们的应用部署到生产环境,我们将使用 GitHub Actions 中的自动化流水线来构建生产镜像。这里一个常见的错误是将长期有效的构建凭据(如我们的 NPM_TOKEN)直接存储为 GitHub 仓库 Secrets。虽然 GitHub Secrets 相当安全,但在规模扩大时往往难以管理,并且可能失去同步。

为了让我们的 Secrets 有一个单一事实来源,我们可以配置工作流,使用官方 GitHub Action 从 Infisical 拉取 Secrets。最棒的是,通过 OpenID Connect (OIDC) 认证,我们无需在 GitHub 中存储任何长期有效的 Infisical 凭据。OIDC 使用一个绑定到你的 GitHub 仓库工作流的临时 token:

.github/workflows/build-push.yaml

name: Build and Push with Infisical Secrets
on:
  push:
    branches: [main]

permissions:
  id-token: write # OIDC 认证到 Infisical 所需
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Get Infisical Secrets
        uses: Infisical/secrets-action@v1.0.16
        with:
          method: "oidc"
          domain: "https://app.infisical.com"
          identity-id: &lt;your_machine_identity_id>
          project-slug: "docker-demo"
          secret-path: "/build"
          env-slug: "prod"

      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          push: false
          tags: demo-app:latest
          secrets: |
            npm_token=${{ env.NPM_TOKEN }}

现在我们已经构建并发布了一个生产就绪的镜像,可以开始部署了。我们可以在生产环境中再次使用 Docker Compose,但 Docker Swarm(Docker 原生的容器编排工具)提供的优势使其更适合生产部署:

  • 多服务器集群
  • 副本与伸缩
  • 滚动更新
  • 负载均衡

生产 Swarm 部署

由于我们的生产容器镜像将部署到 Docker Swarm 集群,我们就需要调整一下将数据库密码从 Infisical 传到应用的方式。因为 Swarm 集群通常使用多个节点,该平台有一个原生的 Secrets API,可以安全地将 Secrets 分发到集群中运行的容器。

为了保持应用配置的简单和一致,我们使用与 Docker Compose 完全相同的 compose.yaml 文件,但这次我们将添加一个小型生产覆盖文件(compose.prod.yaml),告诉 Docker 使用 Secrets API 而不是环境变量。

首先,我们定义覆盖文件,指定 db_password 是一个由 Swarm 管理的外部 secret:

compose.prod.yaml

secrets:
  db_password:
    external: true

接下来,我们使用 Infisical CLI 将 Secrets 实际加载到 Swarm 集群中:

infisical secrets get --path=/deploy DATABASE_PASSWORD --plain | docker secret create db_password -

现在,我们可以使用这些 compose 文件部署应用,Docker 会将 Secrets 注入容器的文件系统。

## 使用两个 compose 文件部署 stack
docker stack deploy -c compose.yaml -c compose.prod.yaml swarm-stack

通过这种方式,我们的 Secrets 在 Infisical 中得到安全管理和轮换,并且生产容器的部署保持简单和一致。

在整个 Docker 生态系统中扩展 Secrets 管理

在容器化环境中管理 Secrets 与在传统虚拟机或裸机服务器上不同。当组织不使用中心化 Secrets 管理器时,通常会退回到临时方法,比如手动配置文件或原始环境变量。这种方法引入了三个主要的安全风险:

  • Secrets 蔓延: 没有单一事实来源,凭据最终会散落在开发者的笔记本电脑、版本控制设置、服务器配置文件和托管平台上。跟踪哪些系统使用哪些凭据变得不可能。
  • 凭据轮换的阻力: 当某个 Secrets 需要更改时,运维人员必须手动更新每个环境文件、重建受影响的 Docker 镜像,并重启所有依赖的容器。这种手动操作负担会导致配置失去同步,并使定期轮换变得不切实际。
  • 缺乏可见性: 去中心化的环境不会生成中心化日志。如果数据库密码或 API token 泄露,就没有办法审计哪个容器、应用或开发者访问过该凭据。

在容器化环境中保护凭据,首先要使用正确的 Docker API,但你可以建立从本地开发到生产环境的统一生命周期,让安全性更上一层楼。通过将 Docker 的原生挂载功能与像 Infisical 这样的中心化 Secrets 管理器相结合,工程团队可以在保持环境安全的同时维持开发速度。

  • 原文链接: infisical.com/blog/docke...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论