19.09-环境变量安全
要点
- 环境变量是所有服务端密钥的载体——API Key、数据库密码、JWT Secret 都存在环境变量里
- 环境变量泄露 = 所有密钥泄露,攻击者获得你的全部服务端权限
- 常见泄露途径:.env 文件提交到 Git、日志打印、错误响应泄露、Docker 镜像暴露、进程列表可见
- Cloudflare Workers 的环境变量分两类:普通变量(明文,放 wrangler.toml)和 Secrets(加密,Dashboard / CLI 设置)
- 环境变量类型安全:用 Zod 在启动时验证环境变量的类型和必填项,防止运行时才发现缺失
- 环境变量命名规范:敏感值用
_SECRET后缀、按服务加前缀、分环境管理 - 最小权限:每个 Worker 只能访问它需要的环境变量,不把所有 Key 都塞给所有 Worker
1. 环境变量的安全风险
环境变量是所有密钥的载体。一旦环境变量泄露,攻击者获得你的全部服务端权限——数据库、第三方 API、云服务、JWT 签名密钥。
1.1 .env 文件提交到 Git
最常见的环境变量泄露方式:
# 项目根目录的 .env 文件
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx
DATABASE_URL=postgres://user:pass@host:5432/db
JWT_SECRET=my-super-secret-key
AWS_SECRET_ACCESS_KEY=AKIAxxxxxxxxxxxxxxxx如果 .env 没有被 .gitignore 排除,git push 之后密钥就公开了。
# .gitignore 必须包含
.env
.env.local
.env.*.local
# 已经提交了的 .env 需要从 Git 历史中清除
# 方法一:BFG Repo-Cleaner
bfg --delete-files .env
# 方法二:git filter-branch
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch .env' \
--prune-empty --tag-name-filter cat -- --all即使从 Git 历史清除了,如果仓库曾经 public 过,密钥可能已经被爬取。一旦泄露,立即轮转所有受影响的密钥。
1.2 日志打印
// 危险:日志里打印了环境变量
console.log('Starting with config:', {
apiKey: process.env.OPENAI_API_KEY,
dbUrl: process.env.DATABASE_URL,
})
// 危险:debug 模式打印所有环境变量
if (DEBUG) {
console.log('Env:', process.env) // ← 所有环境变量
}日志通常会被收集到日志平台(Datadog、CloudWatch、Loki)。一旦环境变量进入日志系统,所有有日志查看权限的人都能看到密钥。
// 正确:日志里只打印非敏感配置
console.log('Starting with config:', {
nodeEnv: process.env.NODE_ENV,
logLevel: process.env.LOG_LEVEL,
apiBaseUrl: process.env.API_BASE_URL,
// 敏感值不打印
})1.3 错误响应泄露
// 危险:错误响应里暴露了环境变量
try {
await connectDB(process.env.DATABASE_URL)
} catch (e) {
return c.json({
error: `Database connection failed: ${process.env.DATABASE_URL}`,
// ← 数据库连接字符串(包含用户名和密码)泄露
}, 500)
}1.4 进程列表
在某些环境下,ps 命令可以看到进程的环境变量:
$ ps -E -p 1234
# 显示进程 1234 的所有环境变量Workers 环境不受此影响——V8 isolate 不暴露进程级别的环境变量。但 Node.js 服务器需要注意。
1.5 Docker 镜像
Docker 构建时的环境变量会被烘焙到镜像层里:
# 危险:ARG 会被烘焙到镜像
ARG DATABASE_URL
RUN echo $DATABASE_URL > /app/config
# 危险:ENV 也会留在镜像里
ENV DATABASE_URL=postgres://...构建时需要的密钥用 Docker BuildKit 的 --secret 或 --mount=type=secret,不会留在最终镜像里。
2. Workers 的环境变量管理
Cloudflare Workers 的环境变量分两类:
2.1 普通变量(Variables)
非敏感配置,明文存储在 wrangler.toml 里:
# wrangler.toml
[vars]
API_BASE_URL = "https://api.example.com"
LOG_LEVEL = "info"
MAX_UPLOAD_SIZE = "10485760"
FEATURE_FLAG_NEW_UI = "true"在代码里通过 env 访问:
export default {
async fetch(request: Request, env: Env) {
console.log(env.API_BASE_URL) // "https://api.example.com"
}
}普通变量在 Dashboard、wrangler.toml、API 里都能看到明文。
2.2 Secrets
敏感值,加密存储,只能通过 Dashboard 或 CLI 设置:
# CLI 设置
wrangler secret put OPENAI_API_KEY
# 交互式输入
# 查看列表(只显示名字)
wrangler secret listinterface Env {
// Secrets
OPENAI_API_KEY: string
JWT_SECRET: string
DATABASE_PASSWORD: string
}Secrets 的特点:
- 加密存储,Dashboard 只显示
•••••••• - 日志里
console.log(env.OPENAI_API_KEY)输出[redacted] - 不能通过
wrangler.toml设置 - 按环境隔离(production、staging、preview)
2.3 Bindings
除了文本类型的环境变量,Workers 还有 Binding——类型化的资源绑定:
# wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxxx"
[[kv_namespaces]]
binding = "CACHE"
namespace_id = "xxxxx"
[[r2_buckets]]
binding = "STORAGE"
bucket_name = "my-bucket"interface Env {
DB: D1Database
CACHE: KVNamespace
STORAGE: R2Bucket
}Binding 不是环境变量——它们是类型化的资源引用,不能当字符串读取。
3. 环境变量类型安全
TypeScript 的类型声明在运行时不生效。如果 wrangler.toml 里漏了某个变量,代码里 env.OPENAI_API_KEY 是 undefined,但 TypeScript 不会报错——直到运行时调用 API 才发现 Key 缺失。
3.1 启动时验证
用 Zod 在 Worker 启动时验证所有环境变量:
import { z } from 'zod'
// 定义环境变量 schema
const envSchema = z.object({
// 必填 Secrets
OPENAI_API_KEY: z.string().min(1, 'OPENAI_API_KEY is required'),
JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 chars'),
DATABASE_PASSWORD: z.string().min(1),
// 可选配置(有默认值)
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
MAX_UPLOAD_SIZE: z.coerce.number().default(10 * 1024 * 1024),
API_BASE_URL: z.string().url().default('https://api.example.com'),
// Bindings(z.any() 因为不是字符串)
DB: z.any(),
CACHE: z.any(),
STORAGE: z.any(),
})
type Env = z.infer<typeof envSchema>
// 验证函数
function validateEnv(env: unknown): Env {
const result = envSchema.safeParse(env)
if (!result.success) {
const errors = result.error.issues
.map(i => ` ${i.path.join('.')}: ${i.message}`)
.join('\n')
throw new Error(`Environment validation failed:\n${errors}`)
}
return result.data
}3.2 在 Worker 入口使用
export default {
async fetch(request: Request, rawEnv: Env): Promise<Response> {
// 每次请求验证一次(确保没有遗漏)
// 或者用一个全局验证只在启动时跑一次
const env = validateEnv(rawEnv)
// 后续代码使用 env,类型安全
const response = await fetch('https://api.openai.com/...', {
headers: { 'Authorization': `Bearer ${env.OPENAI_API_KEY}` },
})
return response
}
}3.3 更严格:启动时一次性验证
如果不想每次请求都验证,用模块顶层验证:
// src/config/env.ts
let validatedEnv: Env | null = null
export function getEnv(rawEnv: unknown): Env {
if (!validatedEnv) {
validatedEnv = validateEnv(rawEnv)
}
return validatedEnv
}4. 环境变量命名规范
4.1 按类别命名
# API Key / Secret
OPENAI_API_KEY=xxx
ANTHROPIC_API_KEY=xxx
JWT_SECRET=xxx
SESSION_SECRET=xxx
# 数据库
DATABASE_URL=xxx
DATABASE_PASSWORD=xxx
REDIS_URL=xxx
# 第三方服务
SENDGRID_API_KEY=xxx
STRIPE_SECRET_KEY=xxx
AWS_SECRET_ACCESS_KEY=xxx
# 功能配置
LOG_LEVEL=info
MAX_UPLOAD_SIZE=10485760
FEATURE_FLAG_NEW_UI=true
# 环境标识
NODE_ENV=production
WORKER_ENV=production4.2 敏感值后缀
用 _SECRET、_KEY、_PASSWORD、_TOKEN 后缀标识敏感值——方便在审计时快速识别需要加密的变量:
# 后缀约定
*_SECRET → JWT Secret、Session Secret
*_KEY → API Key、Access Key
*_PASSWORD → 数据库密码
*_TOKEN → OAuth Token、Bearer Token
*_DSN → 连接字符串(可能包含密码)4.3 按服务加前缀
# 前缀区分来源
OPENAI_* → OpenAI 相关配置
ANTHROPIC_* → Anthropic 相关配置
STRIPE_* → Stripe 支付相关
CLOUDFLARE_* → Cloudflare 相关5. 分环境管理
5.1 Workers 的多环境
Wrangler 支持多环境配置:
# wrangler.toml
name = "my-api"
main = "src/worker.ts"
# 开发环境默认
[vars]
LOG_LEVEL = "debug"
API_BASE_URL = "http://localhost:8787"
# Staging 环境
[env.staging]
name = "my-api-staging"
routes = [
{ pattern = "staging-api.example.com/*", zone_name = "example.com" }
]
[env.staging.vars]
LOG_LEVEL = "info"
API_BASE_URL = "https://staging-api.example.com"
# Production 环境
[env.production]
name = "my-api-production"
routes = [
{ pattern = "api.example.com/*", zone_name = "example.com" }
]
[env.production.vars]
LOG_LEVEL = "warn"
API_BASE_URL = "https://api.example.com"
# Secrets 不在 wrangler.toml 里,通过 CLI 按环境设置
# wrangler secret put OPENAI_API_KEY --env staging
# wrangler secret put OPENAI_API_KEY --env production5.2 环境隔离原则
- Staging 和 Production 用不同的 Key——staging 的 Key 泄露不影响生产
- Preview 环境用测试 Key——测试环境的 Key 权限更小
- 本地开发用
.dev.vars——Wrangler 会自动加载这个文件(已加到 .gitignore)
# .dev.vars(本地开发用,不提交到 Git)
OPENAI_API_KEY=sk-test-xxx
JWT_SECRET=dev-secret-not-for-production6. 最小权限原则在环境变量中的应用
6.1 每个 Worker 只给需要的变量
不要把所有 Key 都塞给每个 Worker:
# 错误:所有 Worker 共享所有 Key
# wrangler.toml for worker-a
[vars]
OPENAI_API_KEY = "xxx"
STRIPE_SECRET_KEY = "xxx"
SENDGRID_API_KEY = "xxx"
DATABASE_PASSWORD = "xxx"
# 正确:每个 Worker 只给需要的 Key
# wrangler.toml for worker-a(AI 接口)
[vars]
OPENAI_API_KEY = "xxx"
# 不需要 STRIPE、SENDGRID、DATABASE_PASSWORD
# wrangler.toml for worker-b(支付接口)
[vars]
STRIPE_SECRET_KEY = "xxx"
# 不需要 OPENAI、SENDGRID6.2 限制 Binding 访问
Workers 的 Binding 也是「环境变量」的一种——Worker 能访问某个 D1 数据库或 KV namespace,是因为 wrangler.toml 里配了 binding。
# 最小权限:每个 Worker 只绑定需要的资源
# AI Worker
[[d1_databases]]
binding = "DB"
database_name = "ai-features" # 只访问 AI 相关的数据库
# 支付 Worker
[[d1_databases]]
binding = "DB"
database_name = "payments" # 只访问支付相关的数据库7. 环境变量安全检查清单
| 检查项 | 说明 |
|---|---|
.env 在 .gitignore 里 | 最基本的安全措施 |
| Secrets 用 Workers Secrets | 加密存储,不放在 wrangler.toml |
| 日志不打印敏感变量 | 只打印非敏感配置 |
| 错误响应不包含环境变量值 | 通用错误信息 |
| 启动时验证环境变量类型 | Zod schema 验证 |
| 分环境使用不同 Key | staging 和 production 隔离 |
本地开发用 .dev.vars | 不提交到 Git |
| 每个 Worker 只给需要的变量 | 最小权限 |
| 敏感值有明确后缀 | _SECRET、_KEY、_PASSWORD |
| 定期审计 Secrets 列表 | wrangler secret list 检查有没有废弃的 Key |
8. 小结
环境变量是所有服务端密钥的载体。泄露 = 全部密钥泄露。
常见泄露途径:.env 提交到 Git、日志打印、错误响应、Docker 镜像。
Workers 的环境变量分三类:普通变量(明文,wrangler.toml)、Secrets(加密,Dashboard/CLI 设置)、Bindings(类型化资源引用)。
类型安全:用 Zod 在启动时验证环境变量的类型和必填项,防止运行时才发现缺失。
命名规范:敏感值用 _SECRET/_KEY 后缀,按服务加前缀,分环境管理。
最小权限:每个 Worker 只给需要的变量和 binding。staging 和 production 用不同的 Key。
下一篇讲 Prompt Injection 防护——AI 应用特有的安全问题,传统 Web 安全管不到。