Skip to content
bitzorcas
中EN

Concept

Data Protection 与共享密钥环

理解 ASP.NET Core Data Protection 的用途隔离、Redis 密钥环、失效模式和滚动发布验证。

Last updated

BitzOrcas 使用 ASP.NET Core Data Protection 保护应用需要“日后解开”的短小机密,例如 AI Provider API Key、Website 线索敏感字段和实验分组标识。它不是密码哈希、TLS、数据库透明加密或云 KMS 的替代品。

保护链路

业务明文

独立 purpose protector

当前 Data Protection key

受保护载荷

Redis 共享密钥环

旧 key 保留

滚动发布仍可解密

数据库或消息

密文通常包含密钥标识和完整性信息。解密方必须拥有同一应用密钥环和相同 purpose 链;仅复制密文或 Redis 连接串不足以互通。

当前注册行为

API 在构建容器前调用 AddBitzOrcasDataProtection。连接串按 ConnectionStrings:redis(Aspire 注入)优先、Redis:ConnectionString 回退的顺序读取。

// 在组合根注册一次,业务模块只依赖 IDataProtectionProvider。
builder.Services.AddBitzOrcasDataProtection(builder.Configuration);
// 每个业务用途创建稳定、独立的 purpose,避免跨域解密。
var protector = provider.CreateProtector(
"BitzOrcas.Website.Leads.SensitiveFields.v1");

没有 Redis 连接串时调用框架默认 AddDataProtection()。当前源码注释把它称为内存密钥环,但实际默认仓库还受运行环境与框架配置影响;说明书把这个分支视为“非共享默认仓库”,不承诺重启或跨实例互通。

存在 Redis 连接串时,注册逻辑在 KeyManagementOptions 上安装 RedisXmlRepository,固定 key 为 bitzorcas:dataprotection:keys。它在运行时解析 IConnectionMultiplexer,用于兼容 Aspire 的延迟连接。

必须理解的回退边界

如果 Redis 连接串存在但容器中没有 IConnectionMultiplexer,配置器会直接返回并保留框架默认仓库,不会让 Host 因此崩溃。这种容错适合本地组合,却意味着“配置存在”不等于“生产密钥环已经共享”。

当前实现还没有给 Data Protection 设置显式 Application Name,也没有在应用层对 Redis 中的 XML key material 再做 KMS/HSM wrapping。跨不同部署共享同一 Redis 前缀会扩大解密边界;生产应通过独立 Redis 账号、网络、数据库或未来的应用名/前缀配置实现隔离。

选择正确的保护方式

数据正确机制原因
用户密码(传输)RSA-OAEP-SHA256 客户端加密 + 服务端解密抗 TLS 终止后 body 日志与重放;见 登录安全
用户密码(存储)专用慢哈希(BCrypt)不需要恢复明文
API Key / SCIM token 的校验值SHA-256 hash + 固定时间比较运行时只需验证
第三方 Provider secretData Protection 或外部 Secret 引用业务需要取回使用
JWT/Webhook 根签名密钥Secret Store / KMS / HSM独立轮换与访问控制
数据库/对象存储平台静态加密大规模持久数据保护
传输内容TLS防止链路窃听与篡改

Data Protection 适合应用拥有、体积小、必须恢复的值。不要用它批量加密大文件,也不要以可逆保护替代密码哈希。

Purpose 设计

Purpose 是密码学隔离的一部分,应包含产品、模块、用途和版本。例如:

// 写入端和读取端必须使用完全相同的 purpose 链。
var protector = provider
.CreateProtector("BitzOrcas.AIManage")
.CreateProtector("ProviderApiKey")
.CreateProtector("v1");
// Protect 的结果可以落库;日志中不得记录原值或解密结果。
var protectedValue = protector.Protect(apiKey);

修改 purpose 等价于切换解密域。需要升级格式时,应先支持读取旧版本并写入新版本,完成迁移后再删除旧读取路径;不能直接重命名常量。

密钥生命周期

新 key 会按框架策略产生,旧 key 必须保留以解开历史载荷。轮换不是删除旧 key,而是让新写入使用新 key,同时保留旧 key 的解密能力。只有确认所有依赖载荷已过期或重加密后,才可撤销历史 key。

Redis key ring 本身也是关键数据:

  1. 只允许应用工作负载账号读写指定前缀;
  2. Redis 链路启用认证和传输保护;
  3. 将 key ring 纳入备份、恢复和变更审计;
  4. 不在普通缓存清理脚本中删除该 key;
  5. 区分开发、测试、预生产和生产的密钥环。

滚动发布演练

发布前用两个不同进程或版本完成双向验证,而不是只在单元测试中用同一 provider round-trip。

旧实例 A Protect(v1) → 新实例 B Unprotect 成功
新实例 B Protect(v1) → 旧实例 A Unprotect 成功
重启 A/B → 两边仍能解开历史探针
暂时阻断 Redis → 告警并确认既定故障策略
恢复 Redis/密钥环 → 历史数据再次可读

如果业务载荷长期存在,还要从数据库抽取一批历史密文做只读验证。探针值应是无业务意义的随机文本,不能把真实 Secret 暴露到测试日志。

故障处理

出现 CryptographicException 时先区分 purpose 不一致、key 丢失、部署使用了不同仓库、载荷损坏和应用版本格式不兼容。不要捕获异常后把密文当作明文继续使用,也不要自动生成新 Secret 掩盖数据丢失。

对于外部连接器,无法解密应把连接标记为不可用并告警;对于实验分组等低风险数据,可以按明确策略重新分配。降级行为必须由具体业务域决定,不能在通用保护层静默吞掉异常。

测试与观测

  • 单元测试验证相同 purpose 可解、不同 purpose 不可解;
  • 集成测试用真实 Redis 与两个 ServiceProvider 做交叉解密;
  • 发布测试覆盖旧版本、新版本和重启;
  • 指标记录解密失败次数和用途分类,但不记录密文或 Secret;
  • Redis key ring 备份恢复有独立演练证据;
  • 运维面能判断当前是否真正使用 Redis repository,而不是默认回退。

当前实现边界

当前注册具备共享 Redis repository 和安全回退,但没有开箱即用的 key-ring 健康探针、显式部署隔离名、KMS wrapping 或强制生产 fail-fast。这些能力属于生产加固项;在它们落地前,多实例验收必须依赖交叉解密和 Redis 运维控制,而不是把注册方法本身视作 GA 证据。

采用结论

单实例开发可接受默认仓库;任何多副本、滚动发布或长期密文场景都必须配置共享仓库并完成交叉解密。监管或高价值 Secret 场景还应把 KMS wrapping 与密钥访问审计列为上线前置条件。

相关主题

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%