Docker 基础体系 · 第 12/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker 配置与 Secret:环境变量、文件注入、轮换和泄漏防护
在 Docker 中,“配置”和“Secret”解决的是两个相似但安全属性不同的问题:
- 配置(configuration):影响程序行为、连接地址、日志级别、功能开关等,通常可以公开或低敏感地管理。
- Secret:能够直接授予权限或身份的值,例如数据库密码、API Token、私钥、签名密钥和云服务凭证。
二者都可能通过环境变量或文件进入容器,但“放进环境变量”并不会自动变成 Secret;“放进文件”也不会自动获得加密、轮换和访问控制。安全性取决于传输路径、存储位置、容器内可见范围、进程生命周期和运维工具的行为。
本文讨论现代 Docker Engine、BuildKit 和 Compose 规范,重点是 Linux 容器边界。Docker Desktop、Windows 容器和 Kubernetes Secret 的实现边界不同,不能直接套用本文结论。
1. 先区分三类数据流
一个 Secret 从运维人员手中到应用进程,通常会经过以下阶段:
flowchart LR
A[凭证来源] --> B[客户端命令或 Compose 文件]
B --> C[Docker API]
C --> D[Engine / Swarm 控制面]
D --> E[容器文件系统或环境]
E --> F[应用进程]
F --> G[日志、崩溃转储、子进程、外部服务]
每一段都有不同的泄漏风险:
- 来源阶段:Shell 历史、CI 日志、配置仓库、密码管理器导出文件。
- 客户端阶段:命令行参数、Compose 插值结果、
docker compose config输出。 - Engine API 阶段:客户端与 Docker daemon 之间的访问权限;远程 daemon 若未正确保护,风险等同于主机控制权风险。
- 容器注入阶段:环境变量、bind mount、Swarm secret mount。
- 应用阶段:错误日志、调试接口、子进程继承、内存转储。
- 运维阶段:
docker inspect、镜像层、备份、监控和日志采集系统。
因此,Secret 管理不是“选择一个字段”的问题,而是要回答:
Secret 在每一个生命周期阶段以什么形式存在,谁能够读取,何时被替换,旧值如何失效?
2. 环境变量是什么,以及它为什么不适合作为高敏感 Secret
2.1 环境变量的语义
Linux 进程启动时会获得一组键值形式的环境变量,例如:
DATABASE_HOST=db
DATABASE_PORT=5432
LOG_LEVEL=info
程序通常通过 getenv() 或语言运行时读取它们。Docker 使用 --env、--env-file 或 Compose 的 environment/env_file 为容器进程设置环境变量。
示例:
docker run --rm \
--env APP_MODE=production \
--env DATABASE_HOST=db.internal \
alpine:3.20 \
sh -c 'printf "mode=%s host=%s\n" "$APP_MODE" "$DATABASE_HOST"'
预期输出:
mode=production host=db.internal
这里的变量只存在于该容器进程及其子进程的环境中。它不是 Docker Secret 对象,也不会因为名字叫 PASSWORD 就获得额外保护。
2.2 环境变量的可见性和复制行为
环境变量有几个重要属性:
-
会进入进程启动配置。Docker daemon 通常能够在容器元数据中记录环境配置。
-
会被子进程继承。应用启动脚本、Shell、迁移工具和外部命令都可能拿到同一份值。
-
容易被诊断工具显示。例如:
docker inspect <container>对于通过容器配置设置的环境变量,输出通常会包含
Config.Env。 -
可能进入崩溃转储、调试快照和错误报告。
-
可能被应用主动打印。常见问题包括启动时打印全部配置、HTTP
/debug/config接口返回环境变量、异常对象包含连接字符串。
在 Linux 上,进程还可能通过 /proc/<pid>/environ 暴露环境内容。能否读取取决于用户身份、进程隔离、ptrace 限制和容器配置,但不能把 /proc 视为可靠的 Secret 保险箱。
2.3 env_file 不是 Secret 管理机制
Compose 中常见:
services:
api:
image: example/api:1.0
env_file:
- .env.production
.env.production 只是由 Compose 客户端读取并传给容器的普通文件。它通常具有以下特点:
- 文件内容可能保存在源码目录、CI 工作区或备份中;
- 变量最终仍然进入容器环境;
docker inspect可能显示最终环境;docker compose config可能渲染出经过插值的配置;- Docker Engine 不会因为文件名包含
.env而加密它。
因此,env_file 适合集中管理普通配置,也可以在某些低风险场景承载凭证,但它不能替代 Secret 机制。
2.4 Compose 插值和容器环境是两个不同阶段
以下 Compose 文件:
services:
api:
image: example/api:1.0
environment:
DATABASE_URL: "postgres://${DB_USER}:${DB_PASSWORD}@db:5432/app"
包含两个阶段:
- Compose 客户端先从 Shell、默认
.env或--env-file获取DB_USER和DB_PASSWORD; - 客户端完成字符串插值后,再把完整的
DATABASE_URL传给容器。
这意味着即使容器内只定义了一个 DATABASE_URL,密码仍然在 Compose 客户端和最终环境配置中出现。运行:
docker compose config
可能直接输出插值后的完整连接字符串。
一个更安全的方向是让应用从文件读取凭证,而不是把完整连接字符串拼进环境变量:
services:
api:
image: example/api:1.0
environment:
DATABASE_HOST: db
DATABASE_PORT: "5432"
DATABASE_NAME: app
DATABASE_USER_FILE: /run/secrets/db_user
DATABASE_PASSWORD_FILE: /run/secrets/db_password
前提是应用确实实现了 _FILE 约定或等价的文件读取逻辑。Docker 不会自动把 DATABASE_PASSWORD_FILE 变成密码;这只是应用约定。
3. 文件注入的核心模型
将 Secret 注入为文件,意味着:
- Docker 在容器文件系统中提供一个路径;
- 应用启动或运行时打开该路径;
- Secret 不必进入进程的初始环境;
- 应用仍然会把读取出的值放入内存,必要时传给数据库或其他服务。
示例应用逻辑可以抽象为:
from pathlib import Path
import os
def read_setting(name: str, default_path: str | None = None) -> str:
path = os.getenv(f"{name}_FILE", default_path)
if path:
return Path(path).read_text(encoding="utf-8").rstrip("\n")
value = os.getenv(name)
if value is None:
raise RuntimeError(f"missing {name} or {name}_FILE")
return value
db_password = read_setting("DATABASE_PASSWORD", "/run/secrets/db_password")
这里的安全属性是:
- 环境变量中保存的是路径,不是密码;
- Secret 文件不需要写入镜像;
- 应用可以在启动时读取一次,也可以在请求前或收到信号后重新读取;
- 文件权限仍然重要,容器内拥有足够权限的进程仍可能读取它。
文件注入并不意味着应用永远不会接触明文。应用必须用明文密码建立认证连接,因此真正要控制的是:
哪个进程能读、明文存在多久、是否被复制到不必要的位置,以及旧凭证何时失效。
4. Compose Secret:开发和单机部署中的文件映射
4.1 一个可运行的 Compose 示例
准备目录:
secret-demo/
├── compose.yaml
└── secrets/
└── db_password
写入测试值:
mkdir -p secret-demo/secrets
printf 'dev-only-password\n' > secret-demo/secrets/db_password
chmod 600 secret-demo/secrets/db_password
cd secret-demo
compose.yaml:
services:
reader:
image: alpine:3.20
command:
- sh
- -ec
- |
printf 'secret path: '
test -f /run/secrets/db_password
echo yes
printf 'secret length: '
wc -c < /run/secrets/db_password
printf 'secret value is not printed\n'
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password
启动:
docker compose up --abort-on-container-exit --exit-code-from reader
预期结果类似:
reader-1 | secret path: yes
reader-1 | secret length: 19
reader-1 | secret value is not printed
db_password 的内容为 dev-only-password\n,长度是 19 个字节。Compose 将该 Secret 暴露在容器内的:
/run/secrets/db_password
其中:
- 顶层
secrets声明 Secret 来源; - 服务级
secrets授权该服务使用它; file指定宿主机上的源文件;- 容器内的路径默认是
/run/secrets/<secret-name>,也可以通过长语法指定target、uid、gid和mode,具体支持程度应以所用 Compose CLI 版本为准。
4.2 Compose 文件 Secret 与 Swarm Secret 不是同一个安全实现
这是最容易混淆的边界。
在普通的 docker compose up 场景中,Compose 通常把本地 Secret 文件映射到容器。它解决的是:
- 不把值写进镜像;
- 不把值直接放进环境变量;
- 通过 Compose 声明哪些服务获得哪个文件。
但宿主机上的源文件仍然是普通文件。Docker Compose 不会自动提供 Swarm 那种控制面加密和集群 Secret 对象语义。
而在 Swarm 中,Secret 是 Docker Engine 的集群资源:
docker swarm init
printf 'production-password-v1\n' | \
docker secret create db_password_v1 -
创建服务:
docker service create \
--name api \
--secret source=db_password_v1,target=db_password \
alpine:3.20 \
sh -c 'while :; do sleep 3600; done'
在任务容器内,Secret 通常以:
/run/secrets/db_password
出现,并由 Swarm 管理其分发。Swarm Secret 的设计包括:
- Secret 只授予明确声明的服务;
- Secret 不写入镜像层;
- Secret 在 Swarm 控制面传输和存储时使用相应的加密保护机制;
- Secret 在任务容器内以只读文件形式提供;
- 删除服务或撤销授权后,任务会被重新安排或更新,从而移除旧授权。
这里的“加密存储”是 Swarm 控制面和 Raft 数据的机制,不等价于“应用内存中的值已加密”,也不等价于“拥有宿主机 root 权限的人无法读取”。宿主机管理员、Docker daemon 管理员和被授予容器访问权的进程仍属于高信任边界。
docker secret 只有在 Swarm 已初始化后才可使用。下面的命令不是普通 standalone Engine 的通用 Secret API:
docker secret ls
如果未启用 Swarm,通常会失败,而不是创建一个本地 Secret 文件。
5. Docker Secret 的权限边界
Secret 的关键安全属性不是“文件存在”,而是“哪些服务和进程能够获得文件”。
Compose 中,只有声明了 Secret 的服务才会被注入:
services:
api:
image: example/api:1.0
secrets:
- db_password
worker:
image: example/worker:1.0
# 没有 secrets,因此不能假设它能读取 db_password
secrets:
db_password:
file: ./secrets/db_password
如果 worker 与 api 共享同一个可写卷,而 api 把密码复制到卷中,那么 Compose 的服务级 Secret 隔离就被应用行为绕过了。类似地,以下做法会扩大暴露面:
services:
api:
volumes:
- shared-data:/data
若应用把 /run/secrets/db_password 复制为 /data/password.txt,任何能读取该卷的服务都可能获得密码。
Linux 容器内的 root 也不是理想的隔离单位。非 root 用户、Capabilities、Seccomp、只读根文件系统等控制可以减少攻击面,但不能改变“被授权的进程可以读取 Secret”这一事实。一个拥有足够权限的进程可能:
- 读取 Secret 文件;
- 附加或调试目标进程;
- 读取共享 PID namespace 中的进程信息;
- 利用应用或内核漏洞突破容器边界。
因此,Secret 授权应当按服务和进程最小化,不能因为容器使用了 read_only: true 就认为 Secret 自动安全。
6. Secret 轮换:更新值不是更新文件
6.1 轮换的严格定义
Secret 轮换至少包含四个动作:
- 创建或生成新凭证;
- 让应用获得新凭证;
- 验证应用已经使用新凭证;
- 使旧凭证失效。
只替换一个文件或修改一个变量,只完成了第 2 步的一部分,不能称为完整轮换。
设当前凭证为 ,新凭证为 。如果服务在时间区间 内仍可能使用 ,认证系统就不能在 立即撤销 。常见安全顺序是:
生成 S1
↓
把 S1 加入认证系统,S0 和 S1 暂时并存
↓
让应用加载并验证 S1
↓
撤销 S0
若认证系统只允许一个活动凭证,则需要先更新应用,再更新服务端认证,或者使用短暂的双凭证窗口,否则轮换会造成中断。
6.2 Swarm 中的不可变 Secret 轮换
Swarm Secret 通常按对象创建后使用。不要把轮换理解为“覆盖 db_password_v1 的内容”;更可靠的模型是创建新名称:
printf 'production-password-v2\n' | \
docker secret create db_password_v2 -
查看服务:
docker service inspect api --pretty
更新服务,使任务改用新 Secret:
docker service update \
--secret-rm db_password_v1 \
--secret-add source=db_password_v2,target=db_password \
api
这里发生了几个状态变化:
-
Service specification 从
db_password_v1改为db_password_v2; -
Swarm 为服务创建更新任务;
-
新任务挂载新的
/run/secrets/db_password; -
旧任务按更新策略停止;
-
旧 Secret 不再被服务授权;
-
运维人员确认所有任务健康后,才撤销数据库中的旧密码;
-
最后删除不再使用的 Secret 对象:
docker secret rm db_password_v1
删除 Secret 对象之前,必须确认没有服务仍然引用它,否则删除可能失败或造成后续更新问题。生产环境还应明确更新策略、并发数、健康检查和回滚行为。如果新凭证无效,服务更新可能处于部分完成状态;此时应根据 docker service ps api 和服务健康状态决定回滚,而不是立即删除旧凭证。
6.3 Compose 中的轮换
Compose 的本地文件 Secret 没有统一的集群轮换控制器。常见流程是:
printf 'dev-password-v2\n' > secrets/db_password.new
chmod 600 secrets/db_password.new
mv secrets/db_password.new secrets/db_password
docker compose up -d --force-recreate reader
mv 在同一文件系统中通常是原子的,但这只保证宿主机目录的替换动作,不保证已经打开旧文件的应用会看到新内容:
- 应用启动时读取一次:必须重启或显式 reload;
- 应用长时间保持文件描述符:替换路径不会改变已经打开的旧 inode;
- 某些 bind mount 文件场景中,宿主机重命名后,容器内挂载仍可能指向原文件对象;
- 应用若支持
SIGHUP或定时重新打开文件,可以减少重启,但必须验证其实现。
因此,Compose 文件 Secret 的轮换通常需要“替换源文件 + 重新创建容器”,而不是只替换宿主机文件。
验证新值时不要打印 Secret:
docker compose exec reader sh -c '
test -s /run/secrets/db_password &&
printf "secret exists, bytes=" &&
wc -c < /run/secrets/db_password
'
还应验证应用本身已经使用新凭证,例如执行一次受控的数据库连接检查,而不是只验证文件存在。
7. 构建时 Secret:BuildKit 与运行时 Secret 是两条链路
运行时 Secret 解决“容器启动后如何获得凭证”;构建时 Secret 解决“构建过程中如何访问私有依赖”。
危险写法:
ARG NPM_TOKEN
RUN npm config set //registry.example/:_authToken="$NPM_TOKEN" \
&& npm install \
&& npm config delete //registry.example/:_authToken
构建参数可能出现在:
- 构建命令;
- 构建缓存元数据;
- 镜像历史;
- 错误输出;
- 生成的配置文件或某一层文件系统。
即使最后删除配置,包含凭证的那一层也可能已经进入镜像历史或缓存。
BuildKit 的 Secret mount 用法:
Dockerfile:
# syntax=docker/dockerfile:1
FROM node:22-alpine AS build
WORKDIR /src
COPY package*.json ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY --from=build /src/dist /usr/share/nginx/html
构建:
DOCKER_BUILDKIT=1 docker build \
--secret id=npmrc,src="$HOME/.npmrc" \
-t example/frontend:build .
机制是:
- 客户端把 Secret 通过 BuildKit 会话发送给构建器;
- 执行带
RUN --mount=type=secret的步骤时,Secret 以临时文件挂载; - 该文件不应进入最终镜像层;
RUN步骤结束后,挂载消失;- 构建结果只保留命令产生的文件。
安全前提是构建命令不能把 Secret 复制出去。例如下面仍然会泄漏:
RUN --mount=type=secret,id=token \
cat /run/secrets/token > /tmp/token.txt
如果 /tmp/token.txt 没有在同一条 RUN 中安全删除,或者被复制到后续阶段,Secret 就进入了构建结果。正确做法是让凭证只用于认证,并确保生成物不包含凭证。
还要区分:
--secret:传递文件或环境形式的构建 Secret;--ssh:转发 SSH agent 或 SSH socket,用于访问私有 Git;ARG/ENV:普通构建参数和环境,不应承载高敏感值。
可以检查镜像历史:
docker history --no-trunc example/frontend:build
这不是证明“绝对没有泄漏”的完整审计,但能发现把 Token 写进 Dockerfile 命令行等明显错误。
8. Secret 泄漏的典型路径
8.1 镜像层泄漏
以下 Dockerfile 会把密码写入镜像层:
FROM alpine:3.20
RUN echo 'password=bad-example' > /app/config.ini
RUN rm /app/config.ini
删除文件只是在新层中标记删除,旧层仍可能包含原内容。镜像导出、缓存、镜像扫描器或拥有镜像读取权限的人可能恢复它。
修复方法不是“最后删除”,而是:
- 构建时使用 BuildKit Secret mount;
- 不把凭证复制到构建产物;
- 使用外部凭证注入;
- 若凭证已经进入镜像或仓库,立即轮换,而不是只删除提交记录。
8.2 命令行和 Shell 历史
危险示例:
docker run --rm -e DB_PASSWORD='real-password' example/api:1.0
即使 Docker 本身不在所有地方保留完整命令行,Shell history、CI 命令回显、进程审计和执行平台日志都可能保留它。
文件方式:
printf '%s' 'real-password' > /tmp/db_password
docker run --rm \
--mount type=bind,src=/tmp/db_password,dst=/run/secrets/db_password,ro \
example/api:1.0
rm -f /tmp/db_password
这减少了命令行暴露,但 /tmp 文件在容器启动前已经存在,且删除后仍可能被备份、审计或取证工具看到。它不是高保证的 Secret 存储,只是避免把值直接写入参数。
8.3 日志泄漏
下面的代码即使使用了文件 Secret,也会泄漏:
PASSWORD="$(cat /run/secrets/db_password)"
echo "connecting with password=$PASSWORD"
应记录非敏感元数据:
PASSWORD="$(cat /run/secrets/db_password)"
echo "database credential loaded; length=${#PASSWORD}"
长度本身也可能属于敏感信息,是否记录取决于威胁模型。更稳妥的是只记录“已加载”和版本标识,不记录内容、长度或哈希。
8.4 连接字符串和错误信息
即使应用没有直接打印密码,数据库客户端错误也可能包含完整 DSN:
dial tcp postgres://user:password@db:5432/app: connection refused
应用应分别传递 Host、Port、User、Password,并对异常信息做脱敏。不要把带密码的 URL 当作普通错误上下文、指标标签或 tracing span 属性。
8.5 调试和诊断工具
以下命令本身具有高权限,执行前应确认输出不会进入工单、聊天记录或 CI 日志:
docker inspect <container>
docker compose config
docker image history --no-trunc <image>
docker logs <container>
诊断 Secret 时优先验证:
docker compose exec api sh -c '
test -r /run/secrets/db_password &&
printf "readable secret file\n"
'
而不是:
docker compose exec api cat /run/secrets/db_password
前者能验证路径和权限,后者会把明文带到终端、终端录制系统和复制粘贴缓存中。
9. 权限、只读根和文件权限的实际关系
Secret 文件通常应当只读。Compose 长语法示意:
services:
api:
image: example/api:1.0
read_only: true
tmpfs:
- /tmp
secrets:
- source: db_password
target: db_password
mode: 0400
secrets:
db_password:
file: ./secrets/db_password
这里有四种不同控制:
read_only: true:使容器根文件系统只读,减少攻击者落地工具和修改系统文件的能力;tmpfs: /tmp:为需要临时写入的目录提供内存文件系统;mode: 0400:限制文件权限,但具体属主和容器用户必须匹配;- Secret 注入:决定文件是否存在于该服务内。
mode: 0400 若文件属主是 root,而应用以非 root 用户运行,应用可能得到:
Permission denied
这是安全配置和可用性之间的直接关系。可以显式设置 uid 和 gid,但 Compose 非 Swarm 实现对这些字段的实际行为可能依赖版本;部署前应在目标 Compose CLI 上验证。不要盲目把权限改成 0444 来“解决问题”,因为这会让容器内更多用户能够读取凭证。
即便根文件系统只读,Secret 文件仍可能可读;只读根不等于 Secret 不可复制。应用本身需要避免将 Secret 写入 /tmp、共享卷或缓存目录。
10. 失败路径与诊断方法
10.1 文件不存在
现象:
open /run/secrets/db_password: no such file or directory
检查顺序:
docker compose config --services
docker compose ps
docker compose exec api ls -l /run/secrets
原因通常是:
- 服务没有在
services.<name>.secrets中声明; - 顶层 Secret 名称拼写不一致;
- 应用使用了错误的
target; - 容器尚未通过 Compose 重建;
- 使用了普通
docker run,却误以为 Compose 的 Secret 声明会自动生效。
10.2 权限被拒绝
现象:
open /run/secrets/db_password: permission denied
检查容器用户:
docker inspect --format '{{.Config.User}}' <container>
在容器内检查:
docker compose exec api id
docker compose exec api ls -ln /run/secrets
重点比较运行用户的 UID/GID 与文件属主及权限。修复时优先调整正确的 UID/GID 和权限,而不是把 Secret 复制到应用用户可读的公共目录。
10.3 应用启动成功但认证失败
文件存在并不证明内容正确。常见原因包括:
- Secret 文件末尾多了换行;
- 应用没有去除换行;
- Secret 的名称对应错版本;
- 数据库已撤销旧密码,但应用仍持有启动时读取的旧值;
- 轮换只更新了容器文件,没有更新认证服务端;
- Secret 注入到了一个没有实际使用的路径。
可以在不打印内容的情况下验证字节数和末尾字节:
docker compose exec api sh -c '
wc -c < /run/secrets/db_password
tail -c 1 /run/secrets/db_password | od -An -t x1
'
如果末尾是 0a,表示包含换行。是否应去掉换行取决于生成工具和应用协议;不能一概而论。
10.4 轮换后旧任务仍在工作
在 Swarm 中检查任务状态:
docker service ps api
重点看:
- 是否仍有旧版本任务处于 Running;
- 更新是否暂停;
- 健康检查是否失败;
- 新任务是否因 Secret 权限或认证失败退出。
在 Compose 中检查容器创建时间:
docker compose ps
docker inspect --format '{{.Created}}' "$(docker compose ps -q api)"
如果只替换了宿主机文件而没有重建容器,应用可能继续使用旧文件描述符或旧进程内存中的凭证。
11. 不同注入方式的安全边界
| 方式 | 典型用途 | 主要暴露面 | 是否自动加密 |
|---|---|---|---|
environment |
普通配置、低敏感参数 | 容器元数据、进程环境、子进程、日志和调试工具 | 否 |
env_file |
批量普通配置 | 源文件、Compose 插值、最终环境和元数据 | 否 |
Compose secrets.file |
单机开发或 Compose 部署 | 宿主机源文件、绑定挂载、拥有 Docker 权限者 | 通常不是 Secret 对象级加密 |
| Swarm Secret | Swarm 服务运行时凭证 | 被授权服务、宿主机高权限边界、应用内存 | Swarm 控制面提供加密保护 |
BuildKit --secret |
构建时访问私有仓库或 API | 构建客户端、构建器、错误输出和错误生成物 | 按 BuildKit 会话处理,不应进入镜像层 |
--build-arg / Dockerfile ARG |
普通构建参数 | 命令、历史、缓存、层和构建日志 | 否 |
表格不能替代具体判断:即使使用 Swarm Secret,如果应用把它打印到日志,仍然会泄漏;即使使用文件注入,如果宿主机源文件被提交到 Git,仍然已经失守。
12. 生产部署中的选择逻辑
可以按部署模式判断:
普通单机 Compose
适合:
- 本地开发;
- 测试环境;
- 单机服务且宿主机文件权限、备份和访问控制已经明确。
建议使用:
secrets:
db_password:
file: ./secrets/db_password
但应把 Secret 文件放在受控目录中,并加入 .gitignore:
secrets/*
.env*
.gitignore 只防止普通 Git 操作误提交,不会删除已经提交的历史,也不限制 CI、备份或宿主机管理员访问。
Swarm
适合需要 Docker 原生集群 Secret 对象、服务级授权和滚动更新语义的场景。应使用版本化 Secret 名称轮换,并结合健康检查和双凭证窗口。
更大规模平台
如果部署平台已经有专门的凭证系统,应让 Docker 容器通过受控代理、节点身份或外部 Secret 管理器获取短期凭证。Docker Secret 不是通用的密码生成、自动轮换和审计系统;它主要负责在 Docker 工作负载边界内提供运行时注入。
13. 一个完整的低泄漏 Compose 模式
下面的模式把普通配置和 Secret 分开:
services:
api:
image: example/api:1.0
user: "10001:10001"
read_only: true
tmpfs:
- /tmp
environment:
APP_ENV: production
DATABASE_HOST: db
DATABASE_PORT: "5432"
DATABASE_NAME: app
DATABASE_USER_FILE: /run/secrets/db_user
DATABASE_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_user
- db_password
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_DB: app
POSTGRES_USER_FILE: /run/secrets/db_user
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_user
- db_password
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10
volumes:
db-data:
secrets:
db_user:
file: ./secrets/db_user
db_password:
file: ./secrets/db_password
这个例子成立有几个前提:
- 使用的 PostgreSQL 镜像支持
POSTGRES_USER_FILE和POSTGRES_PASSWORD_FILE; example/api应用支持对应的_FILE约定;db_user和db_password文件的权限允许容器内实际运行用户读取;read_only不会阻止应用写入必须存在的临时目录,因为/tmp单独使用了 tmpfs;- 数据库持久化数据放在卷中,但 Secret 不应被应用复制到该卷。
需要特别注意,Compose 的 depends_on 健康条件只解决启动顺序和健康检查条件,不解决 Secret 轮换、数据库凭证同步或应用重新加载。服务依赖不是凭证生命周期管理。
14. 最小验证闭环
上线前应验证的是“实际运行状态”,而不是只检查 YAML 是否漂亮:
docker compose config --quiet
docker compose up -d
docker compose ps
然后验证路径和权限,但不输出内容:
docker compose exec api sh -c '
test -r /run/secrets/db_password &&
test ! -r /run/secrets/nonexistent &&
printf "secret path and access check passed\n"
'
验证环境中没有明文密码时,可以检查变量名而非变量值:
docker inspect "$(docker compose ps -q api)" \
--format '{{range .Config.Env}}{{println .}}{{end}}' |
grep -E 'PASSWORD|TOKEN|PRIVATE_KEY' || true
这只能发现常见命名形式,不能证明没有泄漏;完整连接字符串可能使用 DATABASE_URL,而不包含 PASSWORD 字样。
最后检查日志:
docker compose logs --no-color api |
grep -Ei 'password|token|secret|private.key' || true
日志扫描同样只是辅助检测。凭证一旦曾经进入日志,删除当前日志文件也不代表它已经从集中日志、备份、缓存和历史索引中消失;正确处置是立即轮换凭证,并按照日志平台的数据删除和保留策略清理暴露内容。
15. 最终判断标准
选择环境变量还是文件注入,不能只看应用接口是否方便,而应分别回答四个问题:
- 配置是否需要被 Docker 元数据和进程环境直接看到?
- 应用是否能从文件读取,并控制读取时机?
- Secret 来源文件或 Swarm 控制面是否受到合适保护?
- 轮换时应用、认证服务和旧凭证撤销是否有明确顺序?
低敏感配置可以使用环境变量。高敏感凭证通常应优先使用运行时文件注入,构建阶段使用 BuildKit Secret,避免写入镜像层、命令行和日志。单机 Compose 的 secrets.file 改善了应用接口和配置组织,但不应被误认为加密存储;Swarm Secret 提供更强的 Docker 原生对象和分发语义,但仍不能防止已授权进程、宿主机高权限用户或应用自身主动泄漏。
真正完整的 Secret 防护链路是:
不进入源码
→ 不进入镜像层
→ 不进入命令行和普通日志
→ 只授权需要它的服务
→ 应用从受控文件读取
→ 轮换时新旧凭证有明确过渡
→ 验证新凭证生效
→ 撤销并清理旧凭证
缺少其中任何一步,Secret 都可能仍然以另一种形式存在于环境、文件、缓存、日志、进程内存或备份中。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker 资源治理:CPU、内存、PID、I/O、cgroups 与 OOM
- 下一篇:Docker 容器安全:非 root、Capabilities、Seccomp、只读根和隔离
- 延伸:Docker Compose 完整指南:服务、网络、卷、依赖、Profile 和生产边界
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论