feat(security): LLM api_key 端到端加密(AES-256-GCM,磁盘+线缆均密文)

新增 sundynix-shared/secrets:AES-256-GCM,密钥由 SUNDYNIX_SECRET_KEY 经
SHA-256 派生;密文带 enc:1: 版本前缀,历史明文行自动透传(下次保存升级为密文)。

- 网关 SaveModel 加密落库;ListModels/TestModel 解密后脱敏/探测;
  空或脱敏占位的 api_key 视为「未改」→ 沿用库内既有密文(不二次加密)。
- 密文经 NATS 原样下发;消费方解密集中在 bus 层 decryptConfig
  (RequestConfig + SubscribeConfigUpdated)→ dispatcher/mcp-go 零改动。
- secrets.MustHaveKeyInProd():生产未设 SUNDYNIX_SECRET_KEY 直接 fatal,
  gateway/dispatcher/mcp-go 启动各调一次(三服务须配相同密钥)。
- 修复 store.SaveModel 整行 Save 把 active 清零的旧 bug:改 Select(...).Updates
  只覆盖可编辑列,改 key 不再顺手取消模型激活。
- secrets 单测:往返/空串/随机 nonce/错密钥 fail-closed/历史明文透传。
- production_readiness.md 2.1 更新为「已落地」。

验证:DB 列由明文 sk-…(35) → 密文 enc:1:…(90);dispatcher 从密文广播解密后
model config set;真实任务 √256→16、12×12→144 打通 DeepSeek(非降级桩)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-24 10:36:34 +08:00
parent 5cfed55e91
commit 46ef3df221
9 changed files with 274 additions and 20 deletions
+4 -1
View File
@@ -285,7 +285,10 @@ func (p *Postgres) SaveModel(ctx context.Context, m *LLMModel) error {
if m.ID == "" {
return p.db.WithContext(ctx).Create(m).Error
}
return p.db.WithContext(ctx).Save(m).Error
// 更新仅覆盖可编辑列:Active 由 SetActiveModel 单独管理,CreatedAt 不动——
// 避免整行 Save 把 active 清零(改 key/base_url 时不应顺手取消激活)。
return p.db.WithContext(ctx).Model(&LLMModel{}).Where("id = ?", m.ID).
Select("Kind", "Provider", "BaseURL", "APIKey", "Model").Updates(m).Error
}
// SetActiveModel 把指定模型设为激活(同 kind 内其余取消),事务保证每 kind 唯一激活。