一、引言
密钥管理是 Serverless 应用最容易「先松后紧」的环节:起步时把 API Key 写进 .env 就算完事,等密钥泄露、误提交到 GitHub、或者离职同事还握着生产密钥时,才意识到早就该建立规范。
本文讲透 Serverless 平台的配置体系:环境变量的三种形态(构建时 / 运行时 / 绑定)、Vercel 与 Cloudflare 各自的 Secret 管理、多环境与团队治理、密钥轮换与失效策略,最后给出「从裸 .env 到安全密钥体系」的迁移路径。
二、先分清楚环境变量的三种形态
2.1 构建时 vs 运行时 vs 绑定
| 形态 | 注入时机 | 典型场景 | 风险 |
|---|---|---|---|
| 构建时 | 构建阶段注入,打进产物 | 构建 URL、SSG 页面数据 | 进产物 = 泄露面变大 |
| 运行时 | 函数执行时注入 | API Key、数据库地址 | 不可进客户端代码 |
| 绑定 | 平台对象直接绑定 | KV / R2 / D1 / Secret | 最安全,代码无密钥 |
2.2 三条铁律
铁律一:客户端代码里不能有运行时密钥
浏览器能拿到的一切都是公开的——NEXT_PUBLIC_* 只放「本来就公开」的东西
铁律二:密钥不进产物
构建时注入密钥到 SSG 产物 = 把密钥写进每个访问者都能下的文件里
铁律三:能绑定就不放变量
Workers 的 KV/R2/D1 绑定、Secret 绑定,比放环境变量更安全
// 反模式:把密钥打进构建产物(SSG 页面里被所有人看到)
export async function getStaticProps() {
const key = process.env.STRIPE_SECRET_KEY // 会进 HTML/JS 产物!
return { props: { key } }
}
// 正解:运行时在服务端用,绝不进 props
export default async function handler(req, res) {
const charge = await stripe.paymentIntents.create({
amount: 100,
currency: 'usd',
confirm: true,
}, { apiKey: process.env.STRIPE_SECRET_KEY }) // 只在服务端运行时读取
res.json({ id: charge.id })
}
三、Vercel 的环境变量管理
3.1 按环境拆分
Vercel 把环境变量按 Environment 分组:Production / Preview / Development。
# 通过 CLI 管理
vercel env add DATABASE_URL production
vercel env add DATABASE_URL preview
vercel env add DATABASE_URL development
# 查看已有变量
vercel env ls
# 删除(离职/泄露时)
vercel env rm DATABASE_URL production
3.2 在项目里安全引用
// 服务端运行时读取:安全
const dbUrl = process.env.DATABASE_URL
// 客户端公开变量:前缀约定,只放公开数据
const apiBase = process.env.NEXT_PUBLIC_API_BASE
3.3 Vercel 的 Secret 能力
Vercel 用「环境变量 = 引用 + 加密存储」的方式管理密钥,vercel env pull 可以把远程变量拉到本地 .env:
# 把线上配置拉到本地(仅开发环境,注意别提交)
vercel env pull .env.development.local
# 检查 .gitignore:本地 env 必须忽略
# .env*
# !.env.example
心法:
.env.example只放变量名与占位说明(无真实值),是团队「配置即文档」的最小实践——新成员照着它就能跑起来。
四、Cloudflare:Secret 与绑定
4.1 wrangler 管理 Secrets
# 普通环境变量(非敏感,明文可见)
npx wrangler secret put API_KEY
# 交互式输入,或
echo "sk-xxx" | npx wrangler secret put API_KEY --env production
# 列出 / 删除
npx wrangler secrets list
npx wrangler secret delete API_KEY --env production
4.2 在代码里读取
// Workers 里通过 env 参数访问绑定
export default {
async fetch(request, env: { API_KEY: string; KV: KVNamespace }) {
// 运行时才拿到密钥
return Response.json({ masked: env.API_KEY.slice(0, 4) + '****' })
},
}
4.3 本地开发体验
# 本地开发:用 .dev.vars 注入开发密钥(.gitignore 必须忽略)
# .dev.vars
# API_KEY=sk-local-only
npx wrangler dev
安全要点:
- .dev.vars 绝不进 git(wrangler 默认模板已忽略)
- 生产 Secret 用 wrangler secret 管理,不出现在代码里
- 绑定(KV/R2/D1)本身不带密钥,用 Secret 存凭证更稳
五、多环境与团队治理
5.1 环境变量命名与作用域
命名规范:<APP>_<资源>_<用途>
例:STRIPE_SECRET_KEY、SENTRY_DSN、DATABASE_URL
作用域规范:
公开可进客户端 → NEXT_PUBLIC_ 前缀
服务端私有 → 无前缀,只在运行时读取
构建元数据 → 明确标注,禁止含密钥
5.2 团队权限最小化
谁能看值?谁能改值?谁能删?
- 开发者:可看预览环境变量
- 核心维护者:可改生产密钥
- 运维/安全:负责轮换与审计
平台支持:
Vercel:Member 角色按环境授权
Cloudflare:Workers 与账号级权限分离
5.3 配置审计与变更记录
每次密钥变更要有记录:谁、何时、为什么
定期审计:哪些环境变量还活着?谁在引用?
心法:密钥是「高权限资产」,管理它的流程要像管理权限一样严格——最小权限 + 变更留痕 + 定期审计。
六、密钥轮换与失效策略
6.1 轮换的触发时机
强制轮换时机:
- 疑似泄露(误提交到 GitHub、被第三方看到)
- 相关人员离职
- 合规要求(PCI / SOC2 / 年度轮换)
- 服务商安全通告
6.2 无中断轮换流程
轮换步骤(关键:新旧并存 → 切换 → 撤销旧):
1. 生成新密钥,保存为变量(如 STRIPE_SECRET_KEY_V2)
2. 部署使用新变量的版本,灰度观察
3. 确认无报错后,删除旧密钥
4. 旧密钥不留「宽限」,能删则删
# 示例:GitHub Actions + Vercel CLI 自动化轮换(片段)
vercel env add STRIPE_SECRET_KEY_V2 production
# 部署新版本使用 V2
vercel --prod
# 验证通过后删除旧值
vercel env rm STRIPE_SECRET_KEY production
6.3 轮换失败与回滚
- 新密钥在部分地域/功能失败 → 先回滚变量到旧值
- 因此:旧密钥至少保留一个发布周期再删
- 有「失效保护」的密钥服务(如自动过期)优先选择
细节:真正的密钥服务(Vault、云 KMS)支持「版本 + 过期时间」,轮换可自动化;平台 env 的轮换是「手动换值」,流程上要有人把关。
七、加密存储与引用方式
7.1 密钥该放在哪
优先级(安全度从高到低):
① 平台 Secret 管理(Vercel/Cloudflare wrangler secret)——首选
② 外部密钥服务(Vault / AWS Secrets Manager)——企业级
③ 平台环境变量(明文可见)——只放非敏感配置
④ 仓库内 .env(加密且不进 git)——本地开发兜底
⑤ 代码里硬编码——永远禁止
7.2 用外部密钥服务统一管理
// 例:从 Vault 拉取密钥(运行时缓存 + 兜底)
let cachedSecrets: Record<string, string> | null = null
async function getSecret(name: string): Promise<string> {
if (cachedSecrets?.[name]) return cachedSecrets[name]
const res = await fetch(VAULT_URL + '/v1/secret/data/' + name, {
headers: { Authorization: 'Bearer ' + process.env.VAULT_TOKEN },
})
const data = await res.json()
cachedSecrets = data.data.data
return cachedSecrets[name]
}
export default async function handler(req: Request) {
const stripeKey = await getSecret('stripe/live') // 按需获取
return Response.json({ ok: !!stripeKey })
}
7.3 密钥永不落盘的最佳实践
- CI/CD 里用平台的 Secrets(GitHub Actions secrets),不 echo 明文
- 日志/异常信息里对密钥脱敏(只打前 4 位 + ****)
- 密钥禁止进入打包产物、错误堆栈、Sourcemap
- 定期扫描仓库历史,找历史提交里漏网的密钥
// 日志脱敏:异常信息里可能带密钥,统一清洗
function sanitize(str) {
return str.replace(/(sk-[A-Za-z0-9_]{8})[A-Za-z0-9_]+/g, '$1****')
}
console.error(sanitize(err.message))
八、总结
密钥与环境配置的核心要点:
- 分清三种形态:构建时 / 运行时 / 绑定,密钥只进运行时,绝不打进客户端产物。
- 能绑定就不放变量:Workers 的 KV/R2/D1 与 Secret 绑定,比裸环境变量安全。
- 平台 Secret 优先:Vercel env、wrangler secret 是第一选择,外部 Vault/KMS 是企业级选择。
- 多环境隔离:Production / Preview / Development 分开,
NEXT_PUBLIC_前缀只放公开值。 - 团队最小权限:谁看、谁改、谁删,分级授权 + 变更留痕。
- 轮换要无中断:新旧并存 → 切换 → 撤销旧,至少保留一个发布周期。
- 永不落盘:密钥不进 git、不进产物、不打日志,定期扫历史提交。
配置与密钥是 Serverless 应用的「供水管线」——平时看不见,一出事就是大事。把环境变量、Secret、轮换、审计这套流程制度化,再配合 部署与回滚策略 的发布规范与 全球部署与合规 的数据保护要求,你的配置体系才算真正安全可控。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。