如何在Docker中管理密钥
本文深入探讨了在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 | 所有构建自定义容器的团队。 | 镜像泄露:通过构建 ARG 或 ENV 传递 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 中完成一些设置,然后开始我们的示例。你需要:
- 一个免费的 Infisical 账户
- 在 Secrets Management 中创建一个项目
- 两个 Secrets:
/build/NPM_TOKEN和/deploy/DATABASE_PASSWORD(在本示例中,值并不重要) - 一个使用 GitHub Actions 的 OpenID Connect (OIDC) 认证的机器身份
- 在开发工作站上安装 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 镜像时,使用 ARG 或 ENV 等传统变量传递 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: <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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~