Secret 管理不是把值从 appsettings.json 搬到环境变量。生产系统必须知道材料由谁拥有、工作负载如何取得、怎样轮换、旧版本何时撤销、泄露后如何恢复,以及哪些制品和租户受影响。
先按用途分类
| 类型 | 当前例子 | 推荐控制面 |
|---|---|---|
| 对称签名材料 | JWT fallback secret、HMAC、Webhook | Secret Store/KMS,版本化轮换 |
| 非对称验证材料 | Runtime License trusted public keys | 配置可公开但需完整性与版本控制 |
| 连接凭据 | SQL、Redis、RabbitMQ、S3 | 工作负载身份 + Secret Store |
| 外部 Provider | OAuth、短信、邮件、AI API Key | 每 Provider 独立 Secret |
| 应用可逆字段 | Provider key、线索敏感字段 | Data Protection 独立 purpose |
| 校验型凭据 | API Key、SCIM token | 运行态 hash,不保留明文 |
公开密钥不需要保密,但仍不能被未授权替换。许可证验证的 trusted public key 若被攻击者改成自己的公钥,签名校验形式上仍会成功。
配置来源不是保管库
ASP.NET Core 可以从 JSON、user-secrets、环境变量、容器挂载文件或外部 Provider 读取配置。它们是“交付通道”,不自动提供审批、轮换和审计。
# 本地开发使用 user-secrets;值写入用户目录而非仓库。dotnet user-secrets set \ --project src/Hosts/BitzOrcas.Api \ 'Jwt:Secret' '<development-only-secret-at-least-32-chars>'
# 只列出键名用于排查,禁止在共享日志中输出真实值。dotnet user-secrets list --project src/Hosts/BitzOrcas.Api环境变量可能出现在容器描述、诊断快照或崩溃转储。Kubernetes Secret 也只是 API 对象,仍需要 etcd 加密、RBAC、审计和挂载权限。
JWT 密钥环
JWT 启动配置必须有 issuer、audience 和签名材料。没有 Jwt:SigningKeys 时,Jwt:Secret 至少 32 字符;缺失会直接启动失败。生产优先使用带 kid 的版本化 signing keys。
{ "Jwt": { "Issuer": "bitzorcas-api", "Audience": "bitzorcas-client", "SigningKeys": { "2026-07": "<secret-reference-or-injected-value>", "2026-04": "<old-verification-key-during-overlap>" }, "ActiveKeyId": "2026-07" }}轮换顺序是先让所有验证方认识新 kid,再切换签发,等待旧 token 过期,最后撤销旧 key。直接覆盖单一 Secret 会让未过期 token 同时失效,可能造成全量登出。
HMAC、API Key 与 SCIM
HMAC 客户端需要可取回的 secret 来计算签名,因此 Secret Store 必须支持按 client id 定位与轮换。API Key 和 SCIM token 的服务端只需验证,当前运行态采用 SHA-256 hash 映射;管理面应只在创建时展示一次明文。
不要把 key prefix 当作安全材料。boa_live_ / boa_test_ 便于识别环境和审计,真正验证仍依赖完整 key 的 hash 与固定时间比较。
创建:CSPRNG 生成明文 → 向所有者展示一次 → 服务端保存 hash + prefix调用:收到明文 → SHA-256 → 固定时间匹配 → 取得 tenant/scopes撤销:禁用 hash 记录 → 后续请求立即失败兼容性的静态配置适合开发和测试,不等于完整凭据目录。生产还需要 owner、过期时间、scope、最后使用、轮换重叠和撤销审计。
Webhook 与 Provider Secret
Webhook subscription secret 需要发送端取回,当前 repository 通过 IWebhookSecretCipher 加密后落库。Provider API Key 也可通过 Data Protection 做字段级可逆保护。
这类“数据库密文”仍依赖外层 key ring。一旦 key ring 丢失,密文无法恢复;一旦应用身份被攻破,攻击者可能通过正常解密路径取得 Secret。因此数据库备份与密钥环备份必须成对演练,应用权限也要最小化。
Data Protection key ring
API 可把 ASP.NET Core Data Protection key ring 持久化到 Redis 固定 key。Redis 凭据、网络 ACL 和 key-ring 数据本身都属于高敏感控制面。
多环境不可共享同一 key ring。多租户 SaaS 是否共享应用级 ring 是部署决策;即使共享,也必须用独立 purpose 和 tenant-aware 业务访问控制隔离用途。
Runtime License 材料
Production/Staging 的 API Runtime Guard 要求开启 signed runtime licensing,并配置 ProductId、ProductVersion、Environment、TenancyMode、DeploymentIdentityPath、CachePath 和至少一个 TrustedPublicKey。
Trusted public key 可进入受控配置仓库,但私有签发密钥必须留在许可证签发系统的 KMS/HSM 中,绝不能随产品部署。部署身份文件与离线缓存不是普通临时文件:它们需要稳定卷、最小权限和备份策略。
标准轮换流程
- 建立新版本,记录用途、owner、创建与失效时间;
- 验证消费者支持双 key 或多版本读取;
- 先部署验证新 key 的能力;
- 切换签发或主动连接到新版本;
- 观察认证失败、连接失败和旧版本命中;
- 等待最大 token、重试和离线作业窗口;
- 撤销旧版本,并证明旧值不能继续使用;
- 更新资产清单与演练记录。
数据库和 Broker 凭据更适合创建新账号、切流后撤销旧账号,避免原地改密造成同时中断。Webhook/JWT 可使用短暂双验证窗口,但窗口必须有终止时间。
应用读取模式
业务代码依赖 options 或抽象 Provider,不在 Handler 中直接读取环境变量。这样才能替换 Secret Store、测试轮换和集中脱敏。
// 组合根绑定配置并执行启动校验;Handler 只依赖能力接口。builder.Services .AddOptions<ConnectorOptions>() .BindConfiguration("Connectors:Payments") .ValidateDataAnnotations() .ValidateOnStart();
// 日志只记录 provider/key version,不记录 SecretValue。logger.LogInformation("Payment connector uses key version {KeyVersion}", options.KeyVersion);CompositeSecretStore 的优先级与失败语义
基础设施按 DI 注入顺序把多个 ISecretStore 组成读取链,只遍历 IsAvailable=true 的来源。读取规则如下:
| 当前来源结果 | 后续动作 | 全链最终结果 |
|---|---|---|
| 成功 | 立即返回,不再访问低优先级来源 | 成功值 |
ErrorType.NotFound | 继续查低优先级来源 | 所有可用来源都只报 NotFound 时,保留 SecretNotFound |
| 其他失败 | 记录 Warning,继续尝试后续来源 | 后续无成功时返回 NoStoreAvailable |
| 非取消、非 OOM 异常 | 记录 Warning,继续尝试后续来源 | 后续无成功时返回 NoStoreAvailable |
| 没有可用来源 | 无法读取 | NoStoreAvailable |
只有“所有可用来源都确认没有该 Secret”才能返回 NotFound,调用方可据此使用明确的部署默认值。只要链中出现存储故障、未知失败或异常,最终就 fail closed,不能把基础设施故障伪装成“配置未提供”。OperationCanceledException 与 OutOfMemoryException 不会被吞掉。
RotateSecretAsync 也按优先级尝试可用来源,第一个成功即返回;当前写路径不会像读取那样保留 NotFound 分类,若没有来源成功,统一返回 NoStoreAvailable。不要假设读取回退来源就是轮换写入来源,部署清单必须明确哪个 Provider 拥有写权限。
泄露响应
推送到 Git、CI 日志、聊天、工单或公开镜像的 Secret 一律视为泄露。先撤销或轮换,再调查访问;删除消息或重写 Git 历史不能让旧值恢复安全。
响应步骤包括冻结受影响 key、查明时间窗、查询使用记录、轮换派生 token、通知 owner、修复暴露通道并补充检测。若 key 可以解密数据库字段,还要评估历史数据是否已经被读取。
扫描与防泄露
- pre-commit 与 CI 执行 Secret pattern/entropy 扫描;
- 构建产物、容器层和 source map 也纳入扫描;
- ProblemDetails、结构化日志、审计详情和 metric label 做脱敏;
- 配置诊断只输出键是否存在和来源,不输出值;
- 测试 fixture 使用明确的 test prefix,不能复制生产 Secret;
- 截图、录屏和支持包按敏感制品管理。
发布证据
每类 Secret 都要有资产记录、owner、读取主体、轮换周期、上次轮换、撤销方法和恢复演练。关键路径至少演练一次双 key 轮换和一次泄露撤销。
“启动成功”只能证明当前值可用,不能证明最小权限、轮换和审计。商业 GA 的证据应同时包含 Secret Store 策略、Provider 优先级与失败注入、工作负载身份、扫描结果、运行态脱敏和故障演练。