前置阅读:建议先阅读 Docker 生产安全指南 与 Docker Swarm 集群编排实战。
关键概念:环境变量(env)适合非敏感配置,但一进
docker inspect就暴露、且随镜像层固化;Secret 与 Config 是把敏感/非敏感数据作为文件挂载进容器的两类机制——前者加密存储且不落镜像层,后者适合配置文件。生产级密钥管理,还要叠加 Vault 等外部 Secret Manager 的动态密钥与审计。
1. 环境变量:快捷但有边界
1.1 env 的三大局限
局限一:可见性
docker run -e DB_PASSWORD=xxx → docker inspect 容器就能看到明文
任何能读 docker 元数据的人 = 拿到全部密钥
局限二:固化到镜像层
Dockerfile 里 ENV 写死 → 值烙进镜像层
镜像被推送到仓库 = 密钥永久泄露
局限三:无法审计与轮换
改密钥要重新起容器,没有"轮换"的机制
# 用 -e 传参虽方便,但不适合生产密钥
docker run -d -e DB_HOST=db.internal -e DB_USER=appuser \
-e DB_PASSWORD=SuperSecret123 myapp
# 危险示范:把密钥写进 Dockerfile
# ENV DB_PASSWORD=SuperSecret123 # 绝对不要这样
1.2 env 的正确打开方式
env 适合非敏感且需要启动时可变的配置:环境名、日志级别、功能开关、非敏感连接参数。
services:
api:
image: myapp:1.2
environment:
- NODE_ENV=production
- LOG_LEVEL=info
- FEATURE_XX_ENABLED=true
# 数据库主机名等非敏感值可以进 env
- DB_HOST=db.internal
一句话:判断标准只有一个——这个值泄露了会不会出事?会,就走 secret;不会,env 就够。
2. Docker Secrets(Swarm 原生)
2.1 Secret 的存储与注入原理
Docker Secret 是 Swarm 模式的内置能力:secret 内容加密存储在 Raft 日志中,仅目标节点解密,且以 tmpfs 内存文件挂载进容器(绝不写进镜像层和容器可写层)。
创建 secret → 加密写入 Swarm 的 Raft 存储
服务创建引用 secret → 管理器把解密后的值下发到目标节点
目标节点 → 挂载为 /run/secrets/<name>(tmpfs,内存中)
容器删除 → secret 随 tmpfs 消失
2.2 Swarm Secrets 实战
# 从文件创建 secret
printf 'SuperSecret123' | docker secret create db_password -
# 或 docker secret create db_password ./db_password.txt
# 从标准输入创建
echo "postgres://app:apppwd@db:5432/appdb" | docker secret create db_url -
# 部署服务时引用 secret
docker service create \
--secret db_password \
--secret source=db_url,target=app_database_url \
--name api \
myapp:1.2
# 或用 stack 文件声明
version: "3.8"
services:
api:
image: myapp:1.2
secrets:
- db_password
- source: db_url
target: app_database_url
secrets:
db_password:
external: true
db_url:
external: true
容器内读取:/run/secrets/db_password 文件内容是明文,应用读文件即可:
docker exec -it $(docker ps -q -f name=api) cat /run/secrets/db_password
2.3 Secret 的局限
- 只能用于 Swarm 服务,单机
docker run与 Compose 的 secret 是另一套; - 无法跨集群管理(没有轮换、版本化);
- 明文在节点内存与容器文件系统中——需配合文件权限约束。
3. configs:非敏感配置文件的注入
3.1 与 Secret 的分工
| 维度 | Secret | Config |
|---|---|---|
| 适用内容 | 密码、Token、证书私钥 | nginx.conf、log4j.xml、连接池参数 |
| 敏感度 | 敏感 | 非敏感 |
| 存储 | 加密(Raft) | 明文存储 |
| 挂载 | /run/secrets/<name>(tmpfs) | /path/指定位置 |
3.2 configs 使用示例
# 创建 config
docker config create nginx.conf ./nginx.conf
docker config create app.yml ./app.yml
# 服务引用
docker service create \
--config source=app.yml,target=/etc/app/config.yml \
--name api \
myapp:1.2
version: "3.8"
services:
api:
image: myapp:1.2
configs:
- source: app.yml
target: /etc/app/config.yml
mode: 0440
configs:
app.yml:
file: ./app.yml
一句话:规则很简单——会泄露出事的值走 secret,其余文件走 configs;两者都避免了把配置固化进镜像层,改配置只需更新 config/secret 并滚动服务。
4. docker-compose secrets(非 Swarm 也可用)
4.1 compose secrets 的两种来源
单机 Compose(无需 Swarm)支持 secret,可来自文件或环境变量:
services:
app:
image: myapp:1.2
secrets:
- db_password
- api_token
environment:
# 由 secret 文件映射出环境变量(较常见做法)
DB_PASSWORD_FILE: /run/secrets/db_password
env_file:
- .env # 非敏感配置走 env_file
secrets:
db_password:
file: ./secrets/db_password.txt
api_token:
environment: API_TOKEN # 从宿主环境变量注入
4.2 应用读取 secret 的推荐姿势
# 很多框架支持 *_FILE 形式:先读文件再取值
DB_PASSWORD=$(cat "$DB_PASSWORD_FILE")
// Node 应用
const fs = require('fs');
const dbPassword = fs.readFileSync('/run/secrets/db_password', 'utf8').trim();
4.3 权限控制
secrets:
db_password:
file: ./secrets/db_password.txt
# 挂载进容器的 uid:gid 与权限
services:
app:
secrets:
- source: db_password
uid: "1000"
gid: "1000"
mode: 0400
5. 与 HashiCorp Vault 集成
5.1 为什么外部 Secret Manager
内置 secret 解决"不落镜像层",但轮换、动态密钥、访问审计、多环境一致性需要专职 Secret Manager。Vault 是自建主流方案。
Vault 的差异化能力:
- 动态密钥:为每个应用实例签发一次性 DB 凭据,用完即失效
- 密钥版本化与轮换:v1/v2 KV 引擎,支持历史版本
- 审计日志:谁在什么时间读了什么密钥
- 加密即服务:应用只拿加密数据,密钥不进业务进程
5.2 Vault + Docker 的集成方式
方式一:Agent 边车模式(把 secret 拉取成文件)
# 容器启动前用 vault-agent 渲染 /tmp/secrets,再启动应用
# vault-agent.hcl
vault {
address = "https://vault.internal:8200"
role = "app-api"
auth {
type = "kubernetes" # Swarm 场景可用 approle
role = "app-api"
kubernetes_role = "api"
}
}
template {
contents = <<EOF
{{ with secret "kv/data/db" }}{{ .Data.data.db_password }}{{ end }}
EOF
destination = "/tmp/secrets/db_password"
error_on_missing_key = true
}
方式二:应用 SDK 直连(动态密钥最佳)
// 应用启动时从 Vault 拉一次,进程内持有
client, _ := vault.NewClient(nil)
client.SetTokenFromEnv("VAULT_TOKEN") // 或用 AppRole/K8s Auth
secret, _ := client.Logical().Read("kv/data/db")
dbPassword := secret.Data["data"].(map[string]interface{})["db_password"]
5.3 Docker Secrets 与 Vault 的组合模式
推荐组合:
引导凭据(如 Vault 的 role/token)→ Docker Secret 注入
业务凭据(DB/API) → 应用运行时从 Vault 动态获取
Vault 自身凭据 → 存 Secret Manager / KMS 加密
好处:业务密钥不落容器、可动态签发、每次轮换无需重启。
一句话:Docker Secrets 管"静态注入",Vault 管"动态 + 审计 + 轮换"——生产环境两者叠加使用,而非二选一。
6. 云厂商 Secret Manager 集成
6.1 三大云厂商方案对比
| 能力 | AWS Secrets Manager | GCP Secret Manager | Azure Key Vault |
|---|---|---|---|
| 自动轮换 | 支持(Lambda/rotation) | 支持(定时器) | 支持(Key Vault 版本化) |
| 权限模型 | IAM + KMS | IAM + CMEK | RBAC + Managed HSM |
| K8s 集成 | External Secrets / CSI | Secret Manager CSI | CSI driver |
| 典型费用 | 按 secret 数量+调用 | 按版本+访问次数 | 按操作 |
6.2 ECS/EKS 中的 AWS Secrets Manager
# ECS 原生支持引用 secrets(任务定义片段)
# "secrets": [
# {"name": "DB_PASSWORD", "valueFrom": "arn:aws:secretsmanager:...:db_password"}
# ]
# 本地开发用 AWS CLI 获取
aws secretsmanager get-secret-value \
--secret-id arn:aws:secretsmanager:ap-southeast-1:123456:secret:db-password \
--query SecretString --output text
6.3 K8s 上的统一接入:External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: payments
spec:
secretStoreRef:
name: vault-store # 或 aws-secretsmanager-store
kind: SecretStore
target:
name: db-credentials # 落到 K8s Secret
data:
- secretKey: DB_PASSWORD
remoteRef:
key: kv/data/db
property: db_password
一句话:云厂商 Secret Manager + External Secrets/CSI 的价值在于一个来源管理所有环境的密钥,应用侧无感,轮换与回滚都走云平台。
7. 密钥轮换与审计
7.1 轮换的两种模式
| 模式 | 做法 | 风险 | 适用 |
|---|---|---|---|
| 重建式轮换 | 停旧容器→换新密钥→起新容器 | 有停机窗口 | 重启成本低的普通服务 |
| 动态轮换 | 密钥有时效(TTL),到期自动换 | 依赖 Secret Manager | 数据库/短时凭据 |
# 通过更新 secret 并滚动服务完成轮换(Swarm)
docker secret rm db_password
printf 'NewPassword456' | docker secret create db_password -
docker service update --secret-add db_password --secret-rm db_password api
7.2 审计清单
□ 密钥谁创建/谁更新(Secret Manager 操作日志)
□ 谁读取了哪个 secret(Vault audit / 云审计日志)
□ secret 文件在容器内的权限(0400,非 root 可读)
□ 镜像历史里是否残留历史密钥(docker history 检查)
□ 密钥轮换是否自动化(≥90 天一次为宜)
□ 是否误把 secret 打进了镜像(trivy/SecRET 扫描)
# 检查镜像历史是否泄露密钥
docker history --no-trunc myapp:1.2 | grep -iE "password|secret|token"
# 用 gitleaks 扫描仓库与镜像上下文中的密钥
gitleaks detect --source . --verbose
8. 最佳实践清单
□ 敏感值一律 secret,非敏感值走 env/config
□ 绝不把密钥写进 Dockerfile / .env 提交到 git
□ 应用支持 *_FILE 环境变量读取 secret 文件
□ 单机 Compose 用 compose secrets(file/environment 来源)
□ Swarm 生产用 Swarm Secrets + configs
□ 高安全要求接入 Vault / 云厂商 Secret Manager(动态密钥)
□ 密钥轮换自动化并记录审计
□ 定期用 gitleaks/trivy 扫描镜像与仓库残留密钥
一句话:配置与密钥管理的黄金法则——“分层”:非敏感用 env/configs,静态敏感用 secrets,动态敏感用 Secret Manager,并始终把审计与轮换当作一等公民。
9. 总结
| 数据 | 推荐机制 | 存储位置 | 泄露风险 | 生产建议 |
|---|---|---|---|---|
| 非敏感配置(环境/开关) | env / configs | 明文可查 | 低 | 直接使用 |
| 配置文件(nginx/log4j) | configs | 明文存储 | 低 | 用 target 挂载 |
| 静态密钥(密码/Token) | Docker Secrets | 加密 + tmpfs | 中 | Swarm/compose secrets |
| 动态密钥(DB 凭据) | Vault / 云 Secret Manager | 外部加密存储 | 低 | 自动轮换 |
| 引导凭据 | Secret + KMS | 加密 | 低 | 与 KMS 绑定 |
从「-e 一把梭」到「分层治理」,容器密钥管理的成熟度路径是:env → configs/secrets 文件挂载 → Secret Manager 动态密钥 → 全链路审计轮换。每前进一层,都以"密钥不落镜像、不落明文、可轮换、可审计"为验收标准。当密钥泄露不再等于数据库沦陷,你的容器配置体系才算真正生产级。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。