运行时密钥与构建期密钥:容器里的 secret 放在哪一层,会从哪里漏
容器场景下的密钥大致分两类:构建期密钥在构建过程中被消费,用来拉私有依赖、访问受保护的包仓库或云资源;运行期密钥在容器启动或运行时投递,给应用进程用。两者共同的约束是:不要进最终镜像,也不要出现在任何能被随手读到的地方。
为什么 ENV / ARG 不能当密钥
ENV 和 ARG 都会被持久化进最终镜像——ARG 的值留在构建历史里,ENV 直接写进镜像配置。docker inspect 就能看到,镜像分发出去等于把密钥分发出去。官方对 build secret 的定义就是「构建过程中被消费的敏感信息(密码、API token)」,处理方式应该是 secret mounts 或 SSH mounts,而不是 build args、环境变量。
Docker BuildKit:构建期 secret
传递与消费
传递:
docker build --secret id=aws,src=$HOME/.aws/credentials .
Bake 里等价写法:
target "default" {
secret = ["id=aws,src=${HOME}/.aws/credentials"]
}
消费,在 Dockerfile 里挂载:
RUN --mount=type=secret,id=aws AWS_SHARED_CREDENTIALS_FILE=/run/secrets/aws aws s3 cp ...
secret mount 默认把 secret 挂成文件,路径 /run/secrets/。两种改法:
target改文件名:RUN --mount=type=secret,id=aws,target=/root/.aws/credentials ...env挂成环境变量:RUN --mount=type=secret,id=aws-key-id,env=AWS_ACCESS_KEY_ID ...
target 与 env 可以同时使用。
来源类型
来源可以是 file 或 env(type=file / type=env),CLI 和 Bake 会自动探测。几个具体行为:
docker build --secret id=kube,env=KUBECONFIG .
把环境变量 KUBECONFIG 挂成 /run/secrets/kube。
docker build --secret id=API_TOKEN .
省略 env 时,把 API_TOKEN 这个变量绑成 /run/secrets/API_TOKEN 文件。
SSH mount
用于把 SSH agent socket 或私钥带进构建,常见场景是拉私有 Git:
docker buildx build --ssh default .
Dockerfile 里直接:
ADD git@github.com:me/myprivaterepo.git /src/
Git 远程上下文的两个预定义 secret
GIT_AUTH_TOKEN:Basic auth,用户名固定为x-access-token(GitHub 风格)。GIT_AUTH_HEADER:原样作为Authorization头,适配任意 Git 服务商。GitLab 的例子是Basic base64(gitlab-ci-token:$CI_JOB_TOKEN)。
可以按主机分别设置:
--secret id=GIT_AUTH_TOKEN.github.com,env=GITHUB_TOKEN
COPY / ADD 的 HTTP 认证
HTTP_AUTH_TOKEN_ 和 HTTP_AUTH_HEADER_。例如 HTTP_AUTH_TOKEN_127.0.0.1=token 会给发往 127.0.0.1 的请求加上 Authorization: Bearer token。
Docker Compose secrets(运行期)
Compose secrets 的目的就是避免用环境变量存密码:环境变量通常对所有进程可见、访问难以追踪、调试时容易进日志。用法是顶层 secrets 属性定义,服务级 secrets 属性按服务授权,两步走:
services:
myapp:
secrets:
- my_secret
secrets:
my_secret:
file: ./my_secret.txt
密钥以文件形式挂载到容器内 /run/secrets/。
限制一条:只支持 Linux 容器。Compose 通过 bind-mount 单文件投递,而 Windows 容器只支持目录 bind-mount。
很多官方镜像遵循 _FILE 环境变量约定,比如:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
Compose 同时也支持构建期 secret,服务里写 build.secrets: [npm_token],顶层定义 npm_token: environment: NPM_TOKEN。
Docker Swarm secrets(运行期,传输与静态均加密)
Swarm secret 是一段数据 blob(密码、SSH 私钥、TLS 证书等),只分发给被显式授权的服务任务,且仅在这些任务运行时可用。大小上限 500 KB,只对 swarm service 可用,standalone 容器不行。
传输路径:Docker 通过 mutual TLS 把 secret 发给 swarm manager,存在 Raft log 里;Raft log 是加密的,并复制到其它 manager。
运行时行为:授权之后,解密后的 secret 被挂进容器的 in-memory 文件系统,默认 /run/secrets/,Windows 上是 C:\ProgramData\Docker\secrets。任务停止时,解密内容从内存文件系统卸载并从节点内存清除。节点失联时任务仍可用,但收不到更新。
常用命令:
printf "This is a secret" | docker secret create my_secret_data -
docker service create --name redis --secret my_secret_data redis:alpine
docker secret ls
docker secret inspect my_secret_data
有一个能直接说明「secret 不进镜像」的演示结果:对容器 docker commit 之后,新镜像里 /run/secrets/my_secret_data 不存在(cat: No such file or directory)。
正在被 service 使用的 secret 不能直接删:
secret 'my_secret_data' is in use by the following service: redis
要先撤销访问:
docker service update --secret-rm my_secret_data redis
Swarm secret 创建后不可更新,只能删了重建。轮换的实践是在名字里带版本或日期,配合自定义 target 挂载点。
Windows 上没有内置 RAM disk 驱动,secret 会明文落到容器根磁盘,容器停止时被显式移除。缓解办法是对 Docker root 所在卷开 BitLocker。带自定义 target 的 secret 文件不做直接 bind-mount,而是统一放在 C:\ProgramData\Docker\internal\secrets,用符号链接指向目标(实现细节,应用不应依赖),默认目标是 C:\ProgramData\Docker\secrets。Windows 下创建 service 不支持 secret 的 UID/GID/mode。
常见错误做法,各漏在哪里
来自 docker/docker#13490 和 awslabs/ecs-secrets README:
- 环境变量:容易泄漏且未加密,
docker inspect里就能看到。 - 卷:容易被
volumes-from泄漏,也容易在从已有容器创建镜像时泄漏。 - 打进镜像:未加密,容易从构建缓存或镜像分发中泄漏。
对照着看,各投递方式的落点:
| 方式 | 密钥落在哪 | 主要泄漏面 |
|---|---|---|
ENV / ARG | 镜像层、镜像配置、构建历史 | docker inspect、镜像分发 |
| BuildKit secret mount | 构建期的 /run/secrets/ 或指定 target | 写进镜像层(若被 COPY 进层)、构建日志 |
| SSH mount | 构建期 SSH agent socket / 私钥 | 同左 |
| Compose secrets | 容器内 /run/secrets/(bind-mount 单文件) | 宿主机文件权限、Windows 目录挂载限制 |
| Swarm secrets | 运行时内存文件系统 /run/secrets/ | Windows 无 RAM disk 时的根磁盘落盘 |
| ECS(ecs-secrets) | DynamoDB 中的密文 + KMS data key | IAM 权限配置过宽 |
ECS 运行期密钥(awslabs/ecs-secrets)
项目提供管理 ECS 容器 runtime secrets 的开箱方案:CLI + RESTful API,用来创建、轮换、获取、撤销。存储用 DynamoDB 存键值对,加解密用 KMS Data Key,权限用 IAM Users/Roles 控制。
安全目标拆成四块:权限分离(管理密钥的实体和读取密钥的实体分开,读的一方只需要 fetch 权限);加密(DynamoDB 静态数据用 KMS data key 加密,传输走 HTTPS);访问控制(IAM role);版本控制(可注册新版本、撤销旧版本)。
推荐以 sidecar 形式跑 amazon/amazon-ecs-secrets 容器,daemon 模式通过 REST API 访问。
set up 时会建 CloudFormation stack(KMS Master Key + DynamoDB table),并给出 SecretsAdmin(create/rotate/revoke)与 MyApplicationRole(fetch)两套 IAM policy。
创建流程即 KMS 信封加密:
POST secret- 用 Task 的 IAM role 调 KMS
generate-data-key - 用 data key 加密 secret
- 加密后的 secret 与加密的 data key 存入 DynamoDB
撤销指定版本后,fetch 该版本返回 active:false、payload 为空,新版本仍可取。
项目地址:https://github.com/awslabs/ecs-secrets
Kubernetes Secret
Secret 默认只是 base64 编码,不是加密——base64 解码就是明文。要做静态加密需要开 etcd encryption at rest 或 KMS provider(具体细节需自行验证)。
投递方式有两种:挂成 volume(默认 tmpfs)或注入环境变量。环境变量方式同样有泄漏面,和对 ENV 的顾虑一致。
相关最佳实践见官方 "Good practices for Kubernetes Secrets"。
参考链接
- BuildKit build secrets: https://docs.docker.com/build/building/secrets/
- Compose secrets: https://docs.docker.com/compose/how-tos/use-secrets/
- Swarm secrets: https://docs.docker.com/engine/swarm/secrets/
- Kubernetes Secrets: https://kubernetes.io/docs/concepts/configuration/secret/
- awslabs/ecs-secrets: https://github.com/awslabs/ecs-secrets
- docker/docker#13490: https://github.com/docker/docker/issues/13490