在容器化应用交付过程中,密码、证书、API Token 这类敏感信息的传递一直是个棘手问题。如果把它们直接写入 Dockerfile,镜像一旦推送出去就相当于公开了密钥;如果通过环境变量注入,进程列表、调试接口甚至日志都可能把它们暴露出来。Docker Secret 正是为了解决这个场景而设计的一种资源类型,它不属于单机 Docker 命令的常规能力,而是运行在 Swarm 集群模式下的一种安全存储与分发机制。

Secret 的设计思路很直接:敏感数据只在创建时进入集群,之后由 Docker 的 Raft 日志进行加密保存,节点之间通过相互 TLS 通信传输,普通容器无法读取,只有被显式授权的服务才能以只读文件的方式访问。这一机制让密钥的生命周期管理和访问控制都有了清晰的边界,而不是继续依赖镜像层或者环境变量这种随时可能被复制出去的方式。
需要注意的是,Docker Secret 是 Swarm 模式下的功能。如果你只在单机运行 docker run,并没有初始化 Swarm,Secret 相关命令会直接报错。要使用完整的 Secret 能力,需要先执行 docker swarm init,然后通过 docker secret 子命令或 Compose 文件中的 secrets 字段来管理。
Docker Secret 与配置文件的区别
Docker 同时提供了 Config 和 Secret 两种资源,它们在使用方式上很相似,都可以挂载到容器内做成文件,但设计目标完全不同。Config 用来存放非敏感的配置文件,比如 Nginx 的站点配置、应用读取的 JSON 或 YAML 配置;Secret 则专门面向密码、私钥、证书、令牌等必须严格保护的敏感数据。从内部实现看,Secret 在 Raft 日志中默认加密存储,而 Config 一般以明文形式保存,因此在选择资源类型时第一条原则就是:只要数据敏感,一律用 Secret。
两者在挂载行为上也有细微差别。Secret 默认挂载到 /run/secrets/ 目录下,文件内容只有可读权限;Config 默认挂载到 /config-name 这种顶层路径,权限也相对宽松。虽然都可以通过 target、mode、uid、gid 参数自定义挂载位置和权限,但 Secret 的默认约束更严格,符合安全默认值理念。例如,下面分别创建了一个 Config 和一个 Secret,可以直观看到它们在使用时的差异:
# 创建一个普通配置文件
echo "server { listen 80; }" | docker config create nginx.conf -
# 创建一个数据库密码 Secret
echo "MyS3cretP@ss" | docker secret create db_password -
另外,Secret 和 Config 都具备不可变特性。一旦创建成功,内容就无法再修改,只能删除后重新创建。这个设计虽然增加了一点操作成本,但好处是避免了历史版本被悄无声息篡改,配合版本化命名可以实现清晰的密钥轮换流程。
创建和管理 Docker Secret
创建 Secret 最常用的方式是使用 docker secret create 命令。该命令的格式是 docker secret create [OPTIONS] SECRET_NAME FILE_OR_STDIN,其中 FILE_OR_STDIN 可以是一个文件路径,也可以是 - 表示从标准输入读取。生产环境中推荐使用文件方式,因为命令行参数可能被 shell 历史记录或进程监控工具捕获。
# 从文件创建 Secret docker secret create db_password ./db_password.txt # 从标准输入创建,但要注意 shell 历史风险 printf 'MyS3cretP@ss' | docker secret create db_password -
使用 docker secret ls 可以列出当前集群中的所有 Secret,它会显示名称、创建时间等元数据,但不会显示内容。通过 docker secret inspect SECRET_NAME 也只能看到名称、ID、创建时间等基本信息,并不会输出明文数据。这一点保证了 Secret 在管理平面上也不会被轻易读取。
删除 Secret 使用 docker secret rm SECRET_NAME。需要注意的是,如果一个服务正在使用某个 Secret,直接删除操作不会立即影响已经运行的容器,因为 Secret 已经以文件形式挂载进去;但在服务重新调度或滚动更新时就会因为找不到 Secret 而失败。正确的顺序应该是先更新或删除相关服务,再删除 Secret。创建 Secret 后无法修改内容,因此轮换密钥时通常采用创建新版本 Secret 并更新服务引用的方式。
在服务中挂载和使用 Secret
要在容器中使用 Secret,需要在创建服务时通过 --secret 参数显式声明。Docker 服务调度器会负责把 Secret 文件挂载到目标节点的容器内。最简单的方式是直接写 --secret db_password,此时容器内会生成 /run/secrets/db_password 文件,内容就是创建 Secret 时传入的原始数据。
# 创建一个使用 Secret 的 Redis 服务 docker service create \ --name redis \ --secret redis_password \ --publish published=6379,target=6379 \ redis:7 \ redis-server --requirepass /run/secrets/redis_password
上面的例子中,Redis 官方镜像支持通过 --requirepass 指定密码文件,因此我们可以直接让它读取挂载好的 Secret 文件。对于自己开发的应用,通常只需要在代码中读取 /run/secrets/ 目录下的对应文件即可,例如 Python 应用可以这样读取数据库密码:
import os
def get_db_password():
secret_path = "/run/secrets/db_password"
if os.path.exists(secret_path):
with open(secret_path, "r", encoding="utf-8") as f:
return f.read().strip()
return os.environ.get("DB_PASSWORD", "")
password = get_db_password()
除了使用默认路径,还可以通过更细粒度的参数控制挂载行为。例如 --secret source=db_password,target=/etc/app/db_password,mode=0400,uid=1000,gid=1000 可以指定目标路径、文件权限和属主。这种能力在非 root 用户运行的应用中非常有用,可以避免因默认权限导致应用无法读取 Secret 的问题。
docker service create \ --name app \ --secret source=db_password,target=/etc/app/db_password,mode=0400,uid=1000,gid=1000 \ myapp:latest
在 Docker Compose 中,从 3.1 版本开始也支持 secrets 字段。你可以在顶层定义 secrets,然后在服务中按名称引用,并通过 target、mode 等参数定制挂载。下面的示例展示了如何用一个外部 Secret 提供给 WordPress 服务:
version: "3.8"
secrets:
db_password:
external: true
services:
db:
image: mysql:8
secrets:
- db_password
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_password
这里使用了 MYSQL_ROOT_PASSWORD_FILE 环境变量,这是 MySQL 官方镜像提供的从文件读取密码的方式。通过组合 Secret 挂载和镜像内建的文件读取变量,可以完全避免把密码明文放入环境变量。
安全实践与密钥轮换策略
使用 Docker Secret 能解决一部分安全问题,但如果使用方式不当,仍然可能造成泄露。第一条原则是:永远不要把敏感信息写入 Dockerfile 或用 docker build --build-arg 传递。镜像构建参数会被记录在镜像历史中,任何拿到镜像的人都能通过 docker history 查看。第二条原则是:不要用普通环境变量存放密码。环境变量在容器启动后可以通过 /proc/<pid>/environ 读取,很多编排平台也会把环境变量展示在控制台。
Secret 的不可变性带来了一个实际问题:密码轮换时不能直接修改现有 Secret。推荐的做法是给 Secret 名称加上版本号,例如 db_password_v1、db_password_v2,创建新版本后更新服务引用。更新完成后,再删除旧的 Secret。这样可以实现滚动更新,避免因为删除 Secret 导致正在运行的服务调度失败。
# 创建新版本 Secret printf 'NewS3cretP@ss' | docker secret create db_password_v2 - # 更新服务使用新版本 docker service update \ --secret-rm db_password_v1 \ --secret-add source=db_password_v2,target=/run/secrets/db_password \ myapp # 确认无服务使用旧版本后删除 docker secret rm db_password_v1
在集群层面,Docker Swarm 默认对 Raft 日志进行加密,但节点磁盘上的静态数据是否加密取决于底层存储和 Docker 版本。可以通过 docker swarm update --secret-encryption=true 开启 Secret 静态加密,不过该操作会触发一次加密过程,且开启后不能直接反向关闭。对于生产环境,建议在初始化 Swarm 时就规划好加密策略,并配合外部密钥管理服务实现更高等级的密钥保护。
此外,最小权限原则同样适用于 Secret。只给真正需要读取敏感数据的服务挂载 Secret,不要为了方便把所有 Secret 都挂到所有服务上。对于多环境部署,可以使用不同的 Secret 名称或不同的 Swarm 集群来隔离,避免测试环境的密钥与生产环境混用。
常见问题与排查思路
如果在容器内找不到 /run/secrets/ 目录下的文件,首先要确认服务是否运行在 Swarm 模式下。单机执行 docker run 并不支持 Secret 挂载,必须使用 docker service create 或者使用 Swarm 模式下的 Compose 部署。其次检查服务创建时是否使用了 --secret 参数,以及 Compose 文件中的 secrets 定义是否在服务级别正确引用。
遇到权限不足的问题时,可以检查挂载时指定的 mode、uid、gid 是否与应用运行用户匹配。默认 Secret 文件的权限是 0444,只读。如果应用以非 root 用户运行,而挂载目录 /run/secrets 的访问权限限制了该用户,可能需要使用 target 参数把 Secret 挂载到应用可读的位置,或者调整服务的 --user 配置与 Secret 的 uid、gid 对应。
另一个容易混淆的点是 Secret 内容是否会在容器重启后持久化。答案是会,只要服务还在使用该 Secret,容器重新调度或重启后 Docker 会重新挂载文件。但如果 Secret 被删除且服务没有更新,新调度的容器会因找不到 Secret 而启动失败。所以在删除 Secret 前,务必先通过 docker service inspect --pretty 确认哪些服务引用了它。
掌握 Docker Secret 之后,可以进一步结合 Config 资源、Swarm 网络隔离和安全的镜像构建流程,形成一套完整的容器化应用安全基线。在 Kubernetes 环境中也有类似的 Secret 对象,但实现细节和挂载方式有所不同,理解 Docker Secret 的设计思路有助于在不同编排系统之间迁移时把握核心安全原则。
Docker Secret容器安全敏感信息管理修改时间:2026-08-21 15:51:22