降级态密文保护(INV-PERSIST-07)

wesgine 中 CellSpec 密钥降级后的回写保护机制,防止打不开的密文被空值覆盖。


问题场景

缺陷是三步,每步都成功、都不报错:

  1. spec.key 丢失 → decodeSpec 打不开 inline provider key,清空该字段、点名、放 Cell 挂上
  2. Cell 正常跑 → 有人改名 / 加 MCP / 编辑渠道
  3. 覆盖 → 那次写把已成空串的字段重新封密,Seal("") == "",omitempty 丢字段,磁盘上的密文没了

运维后来从备份恢复 spec.key,发现凭据不在。历史里没有一行说是谁花掉的——花掉它的那次写是关于渠道的。


台账机制

两侧台账在修复之前就存在,也已挂上 Observe 快照——只是没有任何写入路径查过它们。

// Hypervisor 持有
h.degradedSecrets      map[string][]string // cellID → 降级字段路径
h.degradedSharedProviders map[string][]string // providerName → 降级字段路径

三扇写门、三种答案

门 1:persistCellSpec(拒绝)

查 h.degradedSecrets[spec.ID]:台账点名的每个字段路径都在本次写入里带非空值才放行。

func (h *cellHandles) persistCellSpec(spec CellSpec) error {
    degraded := h.degradedSecrets[spec.ID]
    if len(degraded) > 0 {
        // 逐字段遍历:台账里的每个路径必须在 spec 中有非空值
        missing := config.WalkSecretRefs(spec, degraded)
        if len(missing) > 0 {
            return ErrDegradedSecretOverwrite // 409
        }
    }
    // ...
}

门 2:Cells().Create(纯 INSERT)

撞行即错,够不到已存在的行——安全性完全建立在"够不到已存在的行"上。

门 3:convergePersistedSpec(原样保留)

把 decode 留下的 SealedOriginals 放回:

func convergePersistedSpec(got persistedSpec, spec CellSpec) {
    // 未被改动的字段:用 got.SealedOriginals 原样放回
    // 已改动的字段:用新值
}

只对刚读出来、未被改动的那份文档成立。


写成功后重算台账

必须做。否则症状比原缺陷更像 bug:

用户重填凭据 → 保存成功 → 下一次不相干的编辑仍被拒,理由是一个现在明明打得开的字段 → 守卫被删 → 缺陷穿着修复的衣服回来。


共享 Provider 池

池的处理与 spec 不同:

Spec共享池
处置拒绝合并
理由整体文档,逐字段合并无定义按名字数组,合并语义数据定义

池拒绝会让运维改 A provider 被 B 的坏 key 扣为人质。

判据放在 SharedProviderStore.Save 内部——两个调用方自动受保护。


封密顺序

封密(Seal)→ 放回保留字节

不是:

放回保留字节 → 封密

secretbox.Seal 对已封密的值直接报错。只对 ref.Value == "" 的字段放回——重填凭据的人必须赢过保留机制。


闸门规则

65-degraded-secret-no-writeback.sh 八条规则:

  1. persistCellSpec 读 h.degradedSecrets[ 且遍历字段
  2. Cells().Create 是纯 INSERT
  3. convergePersistedSpec 传 SealedOriginals
  4. 写门只有三扇——第四条核对数量
  5. 写成功后清台账
  6. 共享池在 Save 内部保留
  7. ON CONFLICT DO UPDATE 不出现在 CREATE 路径
  8. 点名七个残差测试(不是数个数)

反模式

禁止正确做法
降级挂载后 re-seal 整个 specpersistCellSpec 查台账拒绝
守卫函数体改成 return nil闸门正向断言函数体仍有读取
写成功后不清台账台账在 boot 时由 decode 刷新,运行期写入必须自己说
新增第四个 encodeSpec 调用方三扇门三种答案,第四种不适用
给 CREATE 加 ON CONFLICT DO UPDATECreate 的安全性建立在纯 INSERT