掌握Docker中的环境变量:配置与机密
通过掌握环境变量,解锁安全灵活的Docker部署。本全面指南阐明了使用环境变量进行通用应用配置与安全管理API密钥和密码等敏感数据之间的关键区别。学习传递非敏感设置的实际方法,了解通过环境变量暴露机密的严重风险,并发现如何利用Docker Secrets和Compose实现强大、加密的机密管理。提升您的Docker知识,保护您的应用程序。
掌握Docker中的环境变量:配置与机密
环境变量在Docker中非常方便,因为它们允许同一镜像在开发、测试和生产环境中以不同设置运行。当团队将密码、签名密钥和API令牌与日志级别和端口号放在同一个桶中时,这种便利就变得危险。
清晰的思维模型很简单:环境变量适用于非敏感的运行时配置。机密应来自机密存储或挂载的机密文件,并具有有限的访问权限和轮换计划。
理解用于配置的环境变量
环境变量是一种直接且广泛采用的方法,用于向应用程序(包括在Docker容器中运行的应用程序)传递运行时配置。它们允许您在不重建Docker镜像的情况下修改应用程序的行为,从而使您的容器更加灵活和可移植。这对于非敏感的、动态的设置(如应用程序端口号、调试标志或第三方服务URL)非常理想。
传递配置变量的方法
Docker提供了几种定义和向容器注入环境变量的方法:
1. Dockerfile中的ENV指令
ENV指令设置一个默认的环境变量,当容器运行时,该变量将在容器内可用。这适用于不太可能更改的变量或为应用程序提供合理默认值的变量。
FROM alpine:latest
ENV APP_PORT=8080
ENV DEBUG_MODE=false
COPY ./app /app
WORKDIR /app
CMD ["/app/start.sh"]
提示: 虽然ENV设置了默认值,但这些值可以在运行时被覆盖。
2. 使用docker run的-e或--env标志
启动单个容器时,您可以使用-e或--env标志直接传递环境变量。这通常用于临时测试或提供与Dockerfile默认值不同的特定设置。
docker run -d -p 80:8080 --name my_app_instance \
-e APP_PORT=80 \
-e DEBUG_MODE=true \
my_app_image:latest
3. Docker Compose中的env_file
对于管理多个环境变量,尤其是在docker-compose.yml文件中定义的多个服务之间,env_file选项非常方便。它允许您从一个或多个.env文件加载变量,使您的docker-compose.yml更简洁。
docker-compose.yml:
version: '3.8'
services:
webapp:
image: my_app_image:latest
ports:
- "80:8080"
env_file:
- ./config/app.env
./config/app.env:
APP_PORT=8080
DEBUG_MODE=false
API_ENDPOINT=https://api.example.com/v1
4. Docker Compose中的environment键
或者,您可以直接在docker-compose.yml中服务的environment部分定义环境变量。这对于少量变量或特定于单个服务的变量通常更可取。
version: '3.8'
services:
webapp:
image: my_app_image:latest
ports:
- "80:8080"
environment:
APP_PORT: 8080
DEBUG_MODE: false
使用环境变量处理机密的陷阱
虽然环境变量非常适合配置,但它们从根本上不安全,无法管理敏感数据(机密),如数据库密码、API密钥或私有SSH密钥。这是一个经常被忽视的关键安全漏洞,尤其是在开发环境中。
为什么环境变量对机密不安全:
通过
docker inspect可见:任何有权访问Docker主机的人都可以使用docker inspect <container_id>轻松查看运行中容器的环境变量。这意味着您的机密以明文形式清晰可见。# 暴露机密的示例(请勿在生产环境中执行此操作) docker run -d -e DB_PASSWORD=mysecretpassword --name insecure_app nginx:latest # 任何人都可以看到密码 docker inspect insecure_app | grep DB_PASSWORD进程窥探:在容器内部,其他进程或用户(如果存在多个用户)可能能够读取环境变量,尤其是如果应用程序以root身份运行或具有提升的权限。
日志记录和历史:环境变量可能会无意中出现在日志、CI/CD管道历史或shell历史中,导致意外暴露。
镜像层:如果您在Dockerfile中使用
ENV并包含机密,则该机密会被烘焙到镜像层中,即使您尝试在后续层中unset它,它仍然存在。这使得机密可以从镜像本身检索。意外共享:包含机密的
.env文件或docker-compose.yml文件通常会被提交到版本控制系统或不恰当地共享,导致广泛暴露。
警告: 将敏感信息视为常规环境变量是一种常见的安全错误。始终假设环境变量在主机和容器内是公开可见的。
在Docker中安全管理机密
为了解决环境变量处理敏感数据的安全缺陷,Docker提供了专用的机密管理功能,主要通过Docker Secrets(用于Docker Swarm)和外部工具(如具有secrets功能的Docker Compose,它可以利用Docker Swarm机密或简单地挂载文件)。
Docker Secrets(Docker Swarm模式)
Docker Secrets是与Docker Swarm模式集成的功能,提供了一种安全的方式来传输和存储服务的敏感数据。机密:
- 在Swarm管理器的Raft日志中加密存储。
- 安全传输到授权的服务任务。
- 在容器的文件系统中挂载为内存文件,通常位于
/run/secrets/<secret_name>,而不是作为环境变量暴露。 - 仅可由明确授予访问权限的服务访问。
如何使用Docker Secrets(Swarm模式)
- 初始化Swarm(如果尚未初始化):
docker swarm init ```
- 创建机密:机密从文件或标准输入创建。
echo "my_secure_db_password" | docker secret create db_password_secret - echo "SG.your_api_key_here" | docker secret create sendgrid_api_key - ```
- 使用机密部署服务:服务通过名称引用机密。Docker将机密挂载到容器中。
docker service create --name my-webapp
--secret db_password_secret
--secret sendgrid_api_key
my_app_image:latest
```
- 在容器中访问机密:应用程序从挂载的文件路径读取机密。
在您的Python应用程序代码中(或其他语言的类似代码)
with open('/run/secrets/db_password_secret', 'r') as f: db_password = f.read().strip()
with open('/run/secrets/sendgrid_api_key', 'r') as f: sendgrid_key = f.read().strip() ```
Docker Compose和机密(适用于单主机或Swarm)
Docker Compose 3.1+版本引入了secrets部分,允许您在docker-compose.yml中定义和引用机密。在Swarm模式下运行时,Compose利用Docker Swarm的原生机密。在单主机上不使用Swarm模式运行时,Compose仍然支持通过将主机上的文件安全地挂载到容器中来处理机密,尽管没有Swarm提供的静态加密。
在docker-compose.yml中使用secrets
定义机密:您可以通过引用外部文件或将其设为外部机密(预先创建的Swarm机密)来定义机密。
# docker-compose.yml version: '3.8' services: webapp: image: my_app_image:latest ports: - "80:8080" secrets: - db_password - sendgrid_api_key secrets: db_password: file: ./secrets/db_password.txt # 主机上包含密码的文件路径 sendgrid_api_key: external: true # 引用名为'sendgrid_api_key'的现有Docker Swarm机密创建本地机密文件(如果使用
file):
mkdir secrets echo "my_local_db_password" > ./secrets/db_password.txt ```
使用Compose部署:
docker compose up -d将部署您的服务,使机密在容器内的/run/secrets/<secret_name>可用。# 在容器内部,./secrets/db_password.txt的内容将位于: # /run/secrets/db_password
选择合适的工具:配置与机密
决定是使用环境变量进行配置还是使用专用的机密管理解决方案,归结为一个主要问题:
数据是否敏感?
- 如果是(敏感数据): 使用Docker Secrets(与Swarm一起)或类似的机密管理系统(例如Kubernetes Secrets、HashiCorp Vault)。对于单主机Compose设置,使用
secrets部分安全地挂载文件。 - 如果否(非敏感配置): 使用环境变量(通过Dockerfile中的
ENV、-e标志、env_file或Compose中的environment)。
| 特性 | 环境变量(用于配置) | Docker Secrets(用于敏感数据) |
|---|---|---|
| 用途 | 非敏感的应用程序配置 | 敏感数据,如密码和API密钥 |
| 可见性 | 通过docker inspect和进程检查可见 |
挂载为文件;不显示为正常的环境值 |
| 安全性 | 不适用于敏感数据 | 更强的处理;Swarm机密在静态时加密,而本地Compose文件机密依赖于主机文件保护 |
| 应用程序访问 | 从os.environ或类似方式读取 |
从/run/secrets/<secret_name>文件读取 |
| 管理方式 | Docker运行时,Docker Compose | Docker Swarm,Docker Compose,或外部机密管理器 |
| 使用场景 | 端口号,调试标志,非敏感URL | 数据库密码,API令牌,私钥 |
两者的最佳实践
对于配置(环境变量):
- 在Dockerfile中使用
ENV提供合理的默认值。这使您的镜像开箱即用,并清晰地记录了预期的变量。 - 尽可能外部化配置。对于较大的部署,使用带有
docker compose的.env文件或外部配置服务。 - 记录所有配置选项及其预期值,例如在
README.md或应用程序文档中。 - 避免硬编码可能在不同环境(开发、测试、生产)之间变化的值。
对于机密(Docker Secrets及更多):
- 切勿将机密(例如,包含机密的
.env文件、db_password.txt)提交到版本控制系统(如Git)。 - 定期轮换机密。这可以最大限度地减少机密泄露时的暴露窗口。
- 授予最小权限。仅向服务授予其绝对需要的机密访问权限。
- 避免记录机密值。确保您的应用程序和基础设施日志不打印机密内容。
- 对于大规模、企业级部署,考虑使用专用的机密管理解决方案,如HashiCorp Vault、AWS Secrets Manager或Azure Key Vault,它们提供更高级的功能,如审计、动态机密生成以及与身份和访问管理(IAM)的集成。
决定何去何从的实用规则
在将值添加到environment之前,问问自己如果团队成员将其粘贴到支持工单中会发生什么。如果答案是“没什么大不了的”,那可能是配置。如果答案是“我们需要轮换凭证”,那它就是机密。
好的环境变量:
APP_ENV=production
LOG_LEVEL=info
PUBLIC_BASE_URL=https://example.com
FEATURE_SIGNUP_ENABLED=false
REDIS_HOST=redis
坏的环境变量:
DATABASE_PASSWORD=...
STRIPE_SECRET_KEY=...
JWT_SIGNING_KEY=...
AWS_SECRET_ACCESS_KEY=...
PRIVATE_SSH_KEY=...
存在灰色地带。数据库主机名通常是配置。包含用户名和密码的完整数据库URL是机密。公共分析密钥对于浏览器应用程序可能是安全的,而同一供应商的私有API令牌则不是。如有疑问,将值视为敏感,直到证明并非如此。
Compose .env 文件容易被误解
Docker Compose以两种不同的方式使用.env,人们经常混淆。
首先,Compose读取项目级别的.env文件,用于compose.yml内部的变量替换:
services:
web:
image: "${APP_IMAGE}"
ports:
- "${HOST_PORT}:8080"
其次,env_file将变量传递到容器中:
services:
web:
image: my-app
env_file:
- ./app.env
这些文件可能看起来相似,但用途不同。第一个帮助Compose渲染配置。第二个成为容器内的运行时环境。不要假设项目.env中的值会自动出现在容器内,除非您明确传递它。
对于本地开发,一个已检入的示例文件很有帮助:
# .env.example
APP_ENV=development
LOG_LEVEL=debug
PUBLIC_BASE_URL=http://localhost:3000
然后将真实的.env文件排除在Git之外:
.env
*.env.local
secrets/
示例文件记录了应用程序期望的内容,而不会暴露私有值。
在应用程序中读取基于文件的机密
许多应用程序已经期望在环境变量中获取机密。如果您同时支持两种模式一段时间,迁移到基于文件的机密会更容易。
例如,一个Node.js辅助函数:
import fs from "node:fs";
function readSecret(name) {
const filePath = process.env[`${name}_FILE`];
if (filePath) {
return fs.readFileSync(filePath, "utf8").trim();
}
return process.env[name];
}
const databasePassword = readSecret("DATABASE_PASSWORD");
然后您的Compose文件可以指向一个挂载的机密文件:
services:
web:
image: my-app
environment:
DATABASE_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
这种模式效果很好,因为应用程序仍然可以在旧环境中运行,同时您将生产环境迁移到文件挂载的机密。许多官方镜像已经支持以_FILE结尾的变量,正是出于这个原因。
也不要把机密放在构建参数中
环境变量不是唯一的陷阱。如果您使用构建参数来获取私有包或克隆仓库,构建参数也可能泄露:
ARG NPM_TOKEN
RUN npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN
即使最终容器不显示NPM_TOKEN,构建历史和中间层可能仍然会暴露超出您预期的内容。使用BuildKit,为构建时机密使用机密挂载:
# syntax=docker/dockerfile:1.7
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci
像这样构建:
docker build \
--secret id=npm_token,src=.npm-token \
-t my-app .
这样可以将令牌保留在Dockerfile之外,并避免将其烘焙到普通层中。您仍然需要保护本地的.npm-token文件和CI机密存储。
Kubernetes、云机密管理器和Docker
Docker Secrets在Swarm中很有用,Compose机密对于本地或单主机设置很有用。在Kubernetes中,您通常会使用Kubernetes Secrets、外部机密操作器或云机密管理器集成。在AWS上,团队经常使用AWS Secrets Manager或Systems Manager Parameter Store。在Azure上,Azure Key Vault很常见。在Google Cloud上,Secret Manager扮演着相同的角色。
跨平台的原则是相同的:
- 将敏感值存储在专为机密设计的系统中。
- 仅授予运行时身份访问其所需的机密。
- 在运行时挂载或注入机密。
- 轮换机密而无需重建镜像。
- 将机密排除在源代码控制、镜像层、日志和仪表板之外。
Kubernetes Secrets默认是编码的,并非在每个集群配置中都自动加密。许多托管集群支持静态加密,但请验证实际的集群设置,而不是假设。对于高风险凭证,请使用云机密管理器或具有审计日志和轮换支持的专用工具。
轮换是设计的一部分
一个无法轮换的机密策略是不完整的。在生产之前问这些问题:
- 我们能否在不重建镜像的情况下更改数据库密码?
- 在推出过程中,两个有效的凭证能否重叠?
- 应用程序是重新读取机密,还是需要重启?
- 旧凭证记录、缓存或存储在哪里?
- 当机密更改时,谁得到通知?
对于数据库,轮换通常意味着创建第二个凭证,使用新凭证部署应用程序,验证流量,然后撤销旧凭证。对于API密钥,这取决于提供商。一些服务允许多个活动密钥;其他服务则强制切换。围绕最不灵活的依赖项设计您的部署流程。
清理意外暴露
如果机密已经提交到Git或烘焙到镜像中,删除该行是不够的。将其视为已暴露。
通常的响应是:
- 撤销或轮换凭证。
- 从当前代码或镜像中移除它。
- 检查CI日志、镜像注册表、问题跟踪器和聊天消息中是否有副本。
- 仅当您的组织准备好处理协调时才重写Git历史;轮换仍然是必需的。
- 添加扫描或预提交检查以减少重复错误。
工具可以提供帮助,但它们不能替代习惯。清晰地命名机密文件,在Git中忽略它们,并避免在启动时整体打印配置对象。
工作模式
使用环境变量来描述应用程序应如何在此环境中运行的值:端口、日志级别、功能标志、服务主机名和非敏感URL。使用机密来证明身份或授予访问权限的值:密码、令牌、签名密钥、私钥和提供商凭证。
干净的Docker镜像在不同环境中是相同的。开发、测试和生产在运行时改变行为。配置可以作为环境变量传递。机密应来自机密存储或具有有限访问权限的挂载机密文件。这种分离使部署保持灵活,而不会使每个容器检查、日志行或镜像层都成为凭证泄露。