Skip to content

Latest commit

 

History

History
1361 lines (984 loc) · 136 KB

File metadata and controls

1361 lines (984 loc) · 136 KB

Vault 模块现状与改进计划

本文档记录 mega2 当前 vault 模块的实现形态、主要风险、与配置系统的依赖关系,以及后续分阶段改进计划。本文与 config.md 中关于敏感配置、SecretRef、最小 DB/Vault bootstrap 和 core_key.json 加固的约束保持一致。

治理规范:本文档遵循 ../general.md 中定义的统一结构、共同约束和执行标准。在审阅或执行本计划前,请先查阅 general.md 了解共同需求。

集成测试指引:本计划的各阶段应通过 integration.md 中定义的集成测试进行端到端验证,特别是 Vault 初始化、Secret 存储与轮换、fail-closed 行为和最小 bootstrap 能力应在 Docker 环境中完整测试。

仓库格式说明(2026-06-16):当前工作副本由 Libra 管理,不是传统 .git 工作树。执行、评审或核对本计划时,应使用 libra statuslibra diff -- <path>libra add 等 Libra 命令检查工作区状态与差异;不要把 git status / git diff 失败误判为“不是仓库”。本文中“Git 协议 / Git 托管 / 不进入 git”描述的是 mega2 的业务域和兼容目标,不代表当前开发工作区必须由 Git 管理。

事实校准(2026-06-27)

本文档中的代码引用已对照当前 src/(含 src/contract/vault/integration/src/vault vendored 源码、src/context/mod.rssrc/commands)重新核对。以下校准说明(依赖迁移、落地可行性、落地状态更新)按时间顺序记录与早期草案不一致的事实,后续执行以本节、下方「当前实现状态速览表」和「硬约束与不可违反的原则」为准,不要按旧阶段重复实现。

依赖迁移修订(2026-06-15,2026-06-17 路径与模块更新)libvault-core(crates.io 0.1.0)已替换为仓库内 vendored 的 RustyVault 源码模块(src/vault/ 目录,作为 mega2 顶层 module 编译,不再作为独立 path dependency)。新依赖的能力面与旧版不同,本文相关阶段已据此核查与修订,主要影响:

  • 阶段 I(root token 退役 / 最小权限):libvault 已原生提供 ACL policy(modules/policysys/policy/{name})与非 root token(modules/auth/token_store.rsauth/token/create),本阶段从"自建授权体系"改为"接入并编排内建能力"——可行性提升。
  • 阶段 H(审计):libvault 的 sys/audit 仅为桩实现(handler 返回 Ok(None),见 modules/system/mod.rs:883-905),审计须在收窄后的 VaultCoreInterface 上做 hook,不能依赖内建审计设备。
  • 阶段 J(轮换/rekey):unseal 分片 rekey(generate_unseal_keys()core.rs:591)与一次性解封(unseal_once()core.rs:534)可用;但 KEK 轮换无内建原语init() 后 KEK 不可变,无 sys/rotate 等价能力),需另立专项或暂不承诺。
  • PKI:新 libvault 的 PKI 按证书类型分域(tls/ssh/pgp),调用路径已变(如 pki/root/tls/generate/internalpki/ca/tls/pem),src/contract/vault/pki.rs 已随迁移同步。

落地可行性分析补充(2026-06-16):基于对当前代码(src/contract/vault/integration/vault_core.rs、jupiter_backend.rs、context/mod.rs、ssh_server.rs、pgp.rs、nostr.rs、pki.rs 等)、src/vault vendored 源码、其他 refactoring 文档以及 AGENTS.md 的核查,结论如下:

  • vault-only 核心链路(A 止血子集、B、C、F)具备直接落地条件。文档描述的缺陷(root token println+/log::debug+、delete_all on key miss、expect/assert/unwrap 于初始化、JupiterBackend 绑全量 Storage、VaultCoreInterface 暴露 token/raw api、消费端大量 unwrap/panic、reinitialize 测试固化危险行为)与磁盘上代码完全一致,无需等待任何尚不存在的 src/ 组件。
  • libvault 能力核查确认RustyVault::inited() / core.load().inited()unseal_once()generate_unseal_keys() 均可用;policy 与 token 原语(sys/policy/*、auth/token/create)开箱可达;sys/audit handler 存在但按修订说明为桩(handler 返回 Ok(None));PKI 域路径已在 pki.rs 中部分对齐。阶段 H 走 interface hook、I 编排内建、J 分片 rekey 的判断均成立。
  • 跨模块阻塞判断准确src/ 中不存在 redaction 模块、LoadModeSecretRef/resolverconfig secret 命令族,与文档"未实现"声明一致。因此 D/E/G 仍为真阻塞;A 核心止血(删除敏感输出 + fail-closed + 权限 + Result 化)可与 redaction 解耦立即推进。
  • 实施面影响:将 VaultCore::new/config 改为 Result 会要求 AppContext::new 调整(当前为 infallible + expect),并波及 commands/service/*chat_migrate.rs 等调用点。这比"纯 vault 局部"略宽,建议在 A2 切片中显式纳入最小调用方适配,或先在 vault 内部做可失败初始化再由 context 决定上层策略。Storage::new 现已返回 Result,是正向进展(context 仍 expect)。
  • AGENTS.md 硬门禁:任何后续代码变更(即使仅为本计划的 P0 子集)都必须在提交前通过:
    1. cargo +nightly fmt --all --check(无 diff)
    2. cargo clippy --all-targets --all-features -- -D warnings(0 warning/0 error)
    3. source .env.test && cargo test --all(0 失败)。若 .env.test 不存在,必须先询问环境提供者;不得静默回退到无 DB 的 cargo test。 变更必须最小化、复用 jupiter::tests::test_storage / test_db_connection + apply_migrations,禁止新增 blanket #[allow]
  • 其他约束:工作区为 Libra 格式,状态/差异检查用 libra 命令;集成测试按 integration.md 要求在 Docker 中覆盖 fail-closed、最小 bootstrap、初始化路径;备份恢复运行手册必须与 fail-closed 配套(key 丢失场景)。

总体:vault 本地 P0 止血与 P1 结构拆分在当前仓库状态下技术可行、依赖清晰、风险可控;执行时严格按文档内"建议执行切片"与本分析的边界推进,即可避免被跨模块前置或 AGENTS 门禁阻塞。文档其余部分(阶段描述、验收、边界)经本次核查无需结构性调整,仅补充本小节与少量执行提示。

落地状态更新(2026-06-17;2026-06-19 补 H 审计策略与 J secret 轮换命令;2026-06-23 补 H 审计配置化;2026-06-23 补 A6 显式 reset 命令):本轮实现已完成阶段 A/B/C/D/E/F/H/I/J 的可交付子集,并明确阶段 G 的边界。

  • A/B/C/F/H/J:VaultCore 已 Result 化、fail-closed、移除 key 缺失清库路径、收窄 raw API、增加 SecretName 校验、Unix key 权限、DB-only bootstrap、interface 审计 hook、消费端错误传播、unseal share rekey 和恢复运行手册。(2026-06-19)H:audit_secret_access 已 doc-comment 显式记录 fail-open 失败策略;J:新增 config secret rotate 覆写可迁移 secret(首批 mail.password)并显式提示运行中 service 需重启 re-resolve。(2026-06-23)H:审计已配置化——config.vault.audit.enabled(默认开启)经 VaultCore::with_audit_config 注入,满足“审计目的地可配置,默认开启”验收的 enable/default-on 维度;可配置持久化 sink 仍为后续。(2026-06-23)A6:新增 config vault reset --force 显式运维命令,删除 vault 表全部数据、将 core_key.json 按时间戳备份后重新初始化,普通启动不再隐式触发清库。(2026-06-27)J:新增 config vault rekey --force [--key-path] 运维命令,封装既有 VaultCore::rekey_unseal_shares() 重写 unseal 分片(保留数据),补齐阶段 J 第 1 项“提供 unseal 分片 rekey 的运维命令”。(2026-06-28)J/A:新增 config vault backup <DESTINATION> [--key-path <PATH>]config vault restore <SOURCE> --force [--key-path <PATH>] 运维命令,分别把 core_key.json 复制到安全位置(附带 .meta.json、设置 0600 权限)和从备份原子恢复并验证备份 key 能解封当前数据库,补齐阶段 J 第 2–3 项“key 丢失/泄露的备份恢复运行手册”的可执行入口;KEK 轮换仍为后续专项。**

  • (2026-06-24)H:审计记录补 caller 身份(“谁”)。新增 tokio::task_local! AUDIT_CALLERwith_audit_calleraudit_secret_accessvault_audit 事件中记录 caller(未注入时为 unknown);config secret set/check/rotateconfig validate --resolve-secrets、startup/reload 的 mail.password_ref 解析入口已注入对应 caller。由 with_audit_caller_scopes_caller_identity 锁定。

  • **(2026-06-27)A:补齐“root token/分片/secret 明文不得进入日志”验收(vault.md:226/260)的自动化守卫。新增 vault_lifecycle_never_logs_root_token_shares_or_secret_values 回归测试(src/contract/vault/integration/vault_core.rs):用线程局部 tracing subscriber 捕获 VaultCore 集成在 init → write_secret → read_secret → reset 全流程于初始化任务线程上发出的 tracing 事件,断言写入的 secret 明文、已持久化的 unseal 分片(compact JSON / pretty JSON / Debug 三种形态,pretty 对应 persist_core_keyto_writer_pretty)、限权 runtime token 与任何 root-token 字样均不出现;并以变更测试(mutation test)确认注入泄露时该用例会失败。此前阶段 A 仅有 core_key.json 不含 root_token 的持久化断言,无日志侧守卫。该单测的覆盖边界(刻意留白、已在测试注释标注):仅捕获本任务线程上的 tracing 事件,不覆盖 stdout/stderr 的 println!/eprintln!log:: facade(未装 tracing-log 桥)以及 vault 内部后台 OS 线程(如 src/vault/modules/auth/expiration.rs 的租约过期定时线程)上发出的事件——彻底覆盖需全局 subscriber(与其它测试 try_init 竞争)或进程级 fd 捕获,超出本单测范围。

  • D/E:CLI 已引入 LoadModeconfig secret ref/set/checkconfig validate --resolve-secrets 已落地;secret set/check 使用最小 DB/Vault bootstrap,不构造 Redis、对象存储、服务或完整 AppContextSecretRefSecretResolverVaultSecretResolver 已在配置模块落地,mail.password_ref 可在 vault 就绪后解析,且与明文 mail.password 互斥。

  • I:常规 secret 读写不再使用 root token。初始化时用 root token 安装 mega2 运行时 ACL policy、签发 ssh/pgp/nostr/pki/config/generic 限权 token,随后写回不含 root_tokencore_key.json 并撤销 root token。为支持重启后限权 token 的 ACL 校验,vendored libvault 的 token policy 查询增加了 ACL 持久存储 fallback,并移除了明文 token debug 日志。config/generic token 隔离已补矩阵测试:config token 可读 secret/config/*,generic token 显式拒绝 secret/config/*,config token 不能读取 generic secret。

  • G:(2026-06-27)已落地分阶段 bootstrap,对象存储凭据可走 SecretRef;(2026-06-28)validate/CLI 已对齐。 AppContext::new 现只建一次 DB 连接,先用它做 DB-only VaultCore bootstrapfrom_database_connection,不依赖完整 Storage,打破“vault 需要 Storage、Storage 需要对象存储、对象存储凭据需要 vault”的循环),再解析 object_storage.s3.access_key_id/secret_access_key 中的 vault:// SecretRef,最后用解析后的配置 build_object_storage 并经 Storage::new_with_connection(复用同一连接)建完整 storage。Storage::new 本身未拆——通过 DB-only vault bootstrap 达成等效分阶段。字面量凭据原样透传(env/IAM 部署不受影响)。config validateconfig secret set/check 现已接受合法 namespace 的 object_storage SecretRef,config validate --resolve-secrets 也会解析它们。

反向迁移:vendored RustyVault → libvault crate(核对日 2026-08-21,计划 plan-20260820

本节由 docs/plan/plan-20260820.md Task VLT-00 冻结,是「vendored src/vault/ 改用 crates.io libvault 依赖」这一反向迁移的事实源。上文「依赖迁移修订(2026-06-15)」记录的是正向迁移(libvault-core 0.1.0 → vendored),与本节方向相反;两段并存,按日期取后者为当前目标形态。

迁移范围(冻结)

当前事实(核对日 2026-08-21) 目标形态
vendored 源码 src/vault/82.rs + README + 三份 LICENSE;经 src/lib.rs:28-45#[allow(...)] 块 + mod vault; 编译 删除目录与 mod vault;仅当 VLT-S1 = go
vendored 版本 src/vault/mod.rs:69 VERSION = "0.2.2" crates.io libvault = "0.3.0",特性 storage_pg + crypto_adaptor_openssl
错误类型 本地 RvErrorsrc/common/errors/vault.rs:241),经 src/common/errors/mod.rs:23 重导出 libvault::errors::RvError;只读专用变体(ErrCoreReadonlyWriteDenied / ErrCoreReadonlyStateIncomplete)上游不存在,改挂 VaultError
真实引用切换面 src/contract/vault/integration/vault_core.rs:260,368src/contract/vault/pki.rs:80,114(doc-link);RvError 另经 src/contract/vault/integration/jupiter_backend.rssrc/common/errors/ 全部改写为 libvault::*rg 'crate::vault' src bin 零命中
非切换面(误记纠正) src/context/src/server/ssh_server.rssrc/config/src/commands/ 只消费 contract::vault::integrationsrc/jupiter/storage/vault_storage.rssrc/callisto/ 是 SeaORM 实体 不进引用切换写集;只有只读入口签名面随 VLT-02/04 调整
只读原语 Core.readonlysrc/vault/core.rs:110)、Core::new_readonly:182)、MountsRouter::load_readonlyAuthModule::load_auth_readonlyExpirationManager::restore_readonly / is_lease_checker_startedRustyVault::new_readonly——上游零命中 一律不搬迁;只读语义在 src/contract/vault/ 重建(VLT-04)
只读保险 ReadonlyBackendsrc/vault/storage/readonly.rs 移动到 src/contract/vault/integration/readonly_backend.rs 并重挂错误标识
测试 src/vault/fix04_list_contract.rssrc/vault/un31_readonly.rs(挂载于 src/vault/mod.rs:49,59 迁至 src/contract/vault/integration/{fix04_list_contract,un31_readonly}.rs
只读消费点 VaultCore::open_readonlysrc/contract/vault/integration/vault_core.rs:361)→ src/context/mod.rs:436;断言在 src/context/un43_readonly_assembly.rs:300-302bin/tests/integration_authz_audit.rs:919-923 语义不变(UN-31 八 AC 等价)

不在本次反向迁移范围内:vault 表 schema、core_key.json 格式、seal 配置(10/5)、secret/ 挂载路径、mega 的 libvault-core 0.1.0 迁移、libra 的 libvault 升级。

已决议设计决策(摘自 plan-20260820

  • ADR-VLT-01(Accepted):目标形态 = crates.io 的 libvault 0.3.0 crate + src/contract/vault/ 集成层,不继续 fork vendored(方案 A 已拒绝)。默认向上游提 readonly 补丁;plan-long.md 原则 2 保护的是「crate + 集成层」,不是永久 vendored。
  • ADR-VLT-02(Accepted)只读可行性门禁——上游 libvault 0.3.0 的 post_unseal../libvault-rs/src/core.rs:608-631)是私有方法且无 readonly 分支,无条件执行 mounts_router.load_or_defaultmodule_manager.init(后者经 ../libvault-rs/src/modules/auth/mod.rs:411-412 启动过期租约 worker)。因此删除 vendored 必须排在只读可行性结论之后,由 Task VLT-S1 判定 go / no-go。

VLT-S1 go/no-go 门禁(冻结)

结论 触发的后续 禁止的动作
go VLT-02(引用切换 + 只读原语搬迁 + 删 vendored + 测迁)→ VLT-04(UN-31 集成层等价重建)→ VLT-05(REL-VLT-RO 家族唯一发布点) VLT-02 单独推送;以「仅 ReadonlyBackend 拦写」冒充写前 fail-closed
no-go 登记 DEFER-VLT-01,保留 vendored,由 VLT-S2 独立 patch 推送落库;VLT-02/04/05 不进入开工态 删除 src/vault/;结论只留本地不入库

判定必须落到本文档的「VLT-S1」节,写明 结论: go结论: no-go 之一,并附上游 file:line 的逐项否证/采纳表。

UN-31 八条 AC 冻结(等价基线)

来源:docs/plan/plan-20260812.md Task UN-31「Vault readonly bootstrap(R21 P0-1 拆出)」(Lifecycle=done / Acceptance=complete)。方案 B 的只读重建(VLT-04)必须逐条等价,不得弱化;八条与该卡 Acceptance criteria 一一对应。

# 冻结的判据 vendored 当前控制点 迁移后落点
AC1 不补写缺失 / 旧格式 mount:只 load 不 persist(),缺失或「mount_update 会回写」一律写前具名 fail-closed MountsRouter::load_readonly / needs_mount_update 集成层只读引导
AC2 不补写 auth mount,不完整同样写前 fail-closed AuthModule::load_auth_readonly 集成层只读引导
AC3 不补写默认 ACL policy;TokenStore 在 salt 缺失时 fail-closed 而非新签 salt PolicyModule::init 跳过 setup_policytoken_store.rs readonly 分支 集成层只读引导
AC4 禁凭据 rotation:不调用 ensure_runtime_credentials;runtime tokens 不完整即 ReadonlyRuntimeTokensIncomplete;不回写 key 文件、不撤销 root token VaultCore::open_readonly 保持在 VaultCore
AC5 不启动过期租约 worker,且 mounts monitor 完全不创建(无论配置 interval);只读恢复租约(旧格式 fail-closed),禁「普通 restore + 拦写」冒充 ExpirationManager::restore_readonlyis_lease_checker_started 观测面 集成层只读引导;观测面改公共 API
AC6 底层 backend write-denying wrapper:任意 put/delete 硬失败(不是静默 no-op),拒绝计数经 VaultCore::denied_writes() 暴露 src/vault/storage/readonly.rs src/contract/vault/integration/readonly_backend.rs
AC7 故障注入:wrapper 层与 VaultCore 具名层各一组硬失败;具名层拒绝时 denied_writes() == 0 un31_readonly.rs 迁后 integration/un31_readonly.rs
AC8 故障注入:过期租约成对断言——可写侧确实撤销并删除记录,只读侧记录仍在且零写入尝试 un31_readonly.rs 迁后 integration/un31_readonly.rs

守卫口径(冻结):迁移后 un31_ 用例数 ≥ 12,过滤器用位置无关的 un31_(禁用绑定 vendored 模块路径的 vault::un31_);un43_readonly ≥ 4 用例;fix04_list 保持绿。

迁移中间态(VLT-01 已落地,2026-08-21)

libvault crate 与 vendored src/vault/ 现处于双存中间态:依赖已引入、引用未切换、目录未删。这是刻意的中间态——删除待 VLT-S1 判定 go,不由依赖引入触发。双存期间 crate::vault:: 仍是唯一被消费的实现,libvault 只是在依赖图里就位。

  • 依赖行(Cargo.toml):libvault = { version = "0.3.0", features = ["storage_pg", "crypto_adaptor_openssl"] }Cargo.lock 解析到 libvault 0.3.0(registry)。
  • 双版本并存已验证:cargo build 0 错误 0 警告。libvault 带入的次要版本与本仓既有版本并行解析,包括 rand 0.9.5(本仓 0.10.2 + rand08 = 0.8.7)、pgp 0.19.0(本仓 0.20.0)、sqlx 0.8.6(sea-orm 2.0 用 0.9.0)、ureq 2.12.1(本仓 3.3.0)、reqwest 0.12.28(本仓 0.13.4)、strum 0.25.0enum-map 2.7.3pem 3.0.6ipnetwork 0.17.0derive_more 0.99.20hcl-rs 0.18.7radix_trie 0.2.1stretto 0.8.4bcrypt 0.17.1base64 0.22.1。本仓在此之前已有 rand / rand08 双版本先例,故不做单版本收敛(DEFER-VLT-03)。
  • 编译面:storage_pgcrypto_adaptor_openssl 两个特性显式开启;crypto_adaptor_openssl 是上游 default,此处显式写出以免将来 default-features 变化时静默丢失。

VLT-S1:公共 API 只读可行性 spike(2026-08-21)

结论: go。 采用路径 = shadow-unseal:在集成层自行驱动 barrier 解封与 post-unseal 序列,绕开 Core::do_unseal(它会调用私有的 post_unseal),从而根本不去请求 post_unseal 无条件执行的那些修补。不改上游源码,不 fork,不需要 Core.readonly 标志。

时间箱:≤2 人日,实际约 0.4 人日(静态核对 + 一次性原型)。本卡零生产表面改动。

证据:一次性原型(不进本计划提交)

按 ER-03 在仓外一次性 crate 中构建可编译原型(只依赖 crates.io libvault 0.3.0,不依赖 mega2),把 vendored src/vault/un31_readonly.rs 中不依赖 VaultCore 的用例逐条移植,并保留全部成对可写对照。结果:10 passed; 0 failed(另加一条 vendored 未单独覆盖的 AC3 token salt 用例)。原型验证了:只读句柄仍能读出可写句柄写入的 secret;只读引导后 storage 逐字节不变;mounts monitor 与 expiration worker 均未创建;mount 表缺失 / mount 旧格式 / 默认 ACL policy 被删 / 租约旧格式 / token salt 缺失五种情形全部写前具名 fail-closed(对应断言里 denied_writes() == 0,即最终保险从未触发)。

上游锚点:逐项否证 / 采纳表

# 候选路径或关键点 上游锚点 结论
1 ReadonlyBackend + mounts_monitor_interval=0 单独使用 ../libvault-rs/src/core.rs:608-631post_unseal 私有、无条件 load_or_default + module_manager.init 否证:只能写时拒绝,不满足写前具名 fail-closed;worker 仍启动
2 替换 Auth / Policy module(自定义 Module 实现) ../libvault-rs/src/core.rs:279,356,369modules/system/mod.rs:724,781,819,836,971,1072modules/credential/cert/mod.rs:124,134 均以 get_module::<AuthModule>("auth") / <PolicyModule>("policy") 具体类型 downcast 否证:wrapper module 会让这些 downcast 全部落空
3 Shadow-unseal(绕开 Core::do_unseal,自建 readonly post-unseal) 见下「公共 API 清单」 采纳

关键的可行性事实(全部经上游源码核对):

事实 上游锚点
Core 的全部字段为 pubbarrier / mounts_router / module_manager / state / mounts_monitor / mount_entry_hmac_level …) ../libvault-rs/src/core.rs:89-102
RustyVault.corepub ArcSwap<Core> ../libvault-rs/src/lib.rs:70-78
SecurityBarrier trait 公开 unseal / derive_hmac_key / as_storage / inited / sealed ../libvault-rs/src/storage/barrier.rs:17-26
BarrierView::new / new_sub_view 公开;SYSTEM_BARRIER_PREFIXpub const ../libvault-rs/src/storage/barrier_view.rs:51,58mount.rs:40
ShamirSecret::combine 公开 ../libvault-rs/src/shamir.rs:183
MountTable::load 公开且在表缺失时返回 ErrConfigLoadFailed(即 load_or_default 会在此改写并 persist ../libvault-rs/src/mount.rs:299-320
MountTable.entries / MountEntry.table / .hmacpub,可在集成层重建「mount_update 是否需要回写」的判定 ../libvault-rs/src/mount.rs:98,107
ModuleManager::setup / init / get_module / add_module / remove_module 公开 ../libvault-rs/src/module_manager.rs:49,62,85,101,110
只有 AuthModulePolicyModule 覆写 Module::initSystemModule / PkiModule / KvModule / CertModule 只覆写 setup(纯注册,无写),其 init 取 trait 默认 Ok(()) ../libvault-rs/src/modules/mod.rs:78-80modules/{pki,kv,credential/cert}/mod.rsimpl Module
AuthModuletoken_store / expiration / mounts_router / barrierpubadd_auth_backend / setup_auth / load_auth 公开 ../libvault-rs/src/modules/auth/mod.rs:51-57,287,315,326
ExpirationManager::{new,wrap,set_token_store}pub id_view 公开;TokenStore::{new,wrap,new_backend} 公开;PolicyStore::new 公开 auth/expiration.rs:98,152,173,186auth/token_store.rs:150policy/policy_store.rs:224
PolicyStore::new 会从 acl_view.get_keys() 重建 policy_type_map(重启后 ACL 可解析的前提) ../libvault-rs/src/modules/policy/policy_store.rs:233,256-272

采纳的公共 API 序列

  1. RustyVault::new(backend, Some(&cfg)),其中 cfg.mounts_monitor_interval强制置 0——「不创建 mounts monitor」由只读模式决定,与调用方配置无关(AC5 前半)。
  2. core.barrier.inited() / sealed() / seal_config() 预检 → ShamirSecret::combine(阈值为 1 时直接取分片)→ core.barrier.unseal(kek)
  3. 装配 CoreStateCoreState::default() 后写 hmac_key = barrier.derive_hmac_key()system_view = BarrierView::new(barrier, SYSTEM_BARRIER_PREFIX)sealed = false。私有的 kek / unseal_key_shares 不需要——它们只服务 generate_unseal_keys(),那是写路径。
  4. 只读 post-unseal:module_manager.setup(core)mounts_router.mounts.load(...) + 「是否需要 mount_update」扫描(AC1)→ mounts_router.setup(core) → 只读版 auth init(AC2/AC3/AC5)→ 只读版 policy init(跳过 setup_policy,AC3)。
  5. 只读版 auth init 的顺序必须与上游一致: auth.token_store.store(..)才轮到 policy 的 core.add_auth_handler(..)——后者经 AuthModule::set_auth_handlerstoken_store 无条件 unwrap()modules/auth/mod.rs:78-84),顺序颠倒会 panic。

UN-31 八条 AC 在公共 API 上的可断言性

AC 重建手段 观测面(公共 API) 原型断言
AC1 mount MountTable::load + 自建 needs_mount_update 扫描,两种情形均具名 fail-closed 具名错误 + storage 快照 un31_a_missing_mount_table_fails_closedun31_an_older_mount_entry_format_fails_closed
AC2 auth 同上,作用于 auth.mounts_router.mounts,不调用 load_auth 同上 un31_a_readonly_open_persists_nothing 的逐字节快照覆盖
AC3 policy/token 跳过 PolicyModule::setup_policy TokenStore::new 之前预读 system_view.new_sub_view("token/").get("salt"),缺失即具名 fail-closed(而不是让 TokenStore::new 去新签再被拦) 具名错误 + storage 快照 un31_a_deleted_default_policy_is_not_replantedun31_a_missing_token_salt_fails_closed
AC4 rotation 留在 VaultCore::open_readonly,与今日实现一致(不调 ensure_runtime_credentials、不回写 key 文件、不撤 root) VaultError::ReadonlyRuntimeTokensIncomplete 归 VLT-04 的 vault_core 子模块用例
AC5 workers+restore 强制 mounts_monitor_interval = 0调用 AuthModule::init(它是 start_check_expired_lease_entries 的唯一调用点,也是 auth.expiration 的唯一写入点);租约改为只读扫描,旧格式具名 fail-closed core.mounts_monitor.load().is_none() auth.expiration.load().is_none()——后者是直接断言「从未走过启动 worker 的那条路径」,不是从「没观察到撤销」倒推 un31_a_readonly_open_starts_no_background_workerun31_an_older_format_lease_fails_closedun31_an_expired_lease_survives_a_readonly_open(均带可写对照)
AC6 wrapper ReadonlyBackend 迁至集成层;put/delete 硬失败 denied_writes() un31_the_readonly_backend_denies_put_and_delete
AC7 双层硬失败 wrapper 层 + VaultCore 具名层 具名层拒绝时 denied_writes() == 0 wrapper 侧已证;具名层归 VLT-04
AC8 过期租约成对 只读扫描不注册、不启 worker storage 中记录仍在 vs 可写侧被撤销 un31_an_expired_lease_survives_a_readonly_open

替代观测面登记(AC5):vendored 的 ExpirationManager::is_lease_checker_started() 上游零命中,且 start_check_expired_lease_entries 不返回句柄、stop_... 只清队列(auth/expiration.rs:480,541),故上游侧无法直接问「线程起了没有」。替代面为 AuthModule.expiration.load().is_none()auth.expiration 在上游AuthModule::init 写入(modules/auth/mod.rs:388),此外全仓无读者,因此它为空等价于「AuthModule::init 未运行」,也就等价于 worker 未启动。该等价关系须由一条源码级零命中守卫加固(集成层不得出现 start_check_expired_lease_entries)。

已知脆弱点(drift risk,须随 VLT-04 登记)

# 脆弱点 缓解
1 只读 post-unseal 是上游私有 post_unseal 的影子实现;上游若在其中新增步骤,只读路径不会自动跟进 UN-31 十二用例 + 逐字节快照断言;升级 libvault 时按本节复核
2 token salt 路径 "token/" + "salt" 是上游私有常量 TOKEN_SUB_PATH / TOKEN_SALT_LOCATIONauth/token_store.rs:51-52)的字面量复制 un31_a_missing_token_salt_fails_closed 带可写对照——上游若改路径,可写对照会先红
3 租约旧格式判定复制了私有 LeaseEntry 的 serde 形状(data 必须是对象);LeaseEntry/OldLeaseEntry 上游为私有类型 libvault::utils::deserialize_system_time 保持时间字段一致;un31_an_older_format_lease_fails_closed 带可写对照
4 set_auth_handlerstoken_store 无条件 unwrap(),只读 init 的顺序不能变 顺序写入代码注释 + un31_a_readonly_open_reads_what_the_writable_one_stored 会在顺序错时 panic

额外发现:两处与只读无关的 vendored 分歧(执行期核对,2026-08-21)

计划「事实基线」把 vendored↔上游差异记为「UN-31 只读 + RvError 本地化 + 依赖版本面」。逐文件核对(剔除纯 import 改写与 doc 后共 24 个文件有差异)另发现两处功能性分歧,不在只读轴内,必须随 VLT-02/04 处置:

  1. modules/policy/policy_store.rs:331:vendored 在 get_policy(_, PolicyType::Token) 未命中 policy_type_map 时回落到持久 ACL view(policy_type = Acl; (Some(self.get_acl_view()?), &self.token_policies_lru)),上游为 (None, &None)(随后因 view.is_none() 报错)。这是 FIX-04 之前 JupiterBackend::list 返回递归全 key 导致 PolicyStore::new 无法正确重建 policy_type_map 的历史绕行;FIX-04 已修复该 list 契约,故上游行为预期已足够。判据:切换后 integration_vault / integration_authz_audit 以及限权 token 隔离矩阵必须仍绿——这是 VLT-02 的强制回归观测点,不是可选项。
  2. modules/auth/token_store.rs:457:上游为 log::debug!("check token: {token}"),会把明文 client token 写进 debug 日志;vendored 已脱敏为 log::debug!("check token")。本仓未安装任何 log facade logger、也未接 tracing-logrg 'tracing_log|LogTracer|log::set_logger' src bin 零命中),因此该语句在 mega2 内不产生输出,属残留风险而非现存泄漏;一旦将来接入 log 桥必须重新评估(登记为 DEFER-VLT-04)。

VLT-04 落点估计

生产落点 2 个文件,与计划的 scope=S 一致:

  • src/contract/vault/integration/vault_core.rsopen_readonly 内的 shadow-unseal 序列(预计 +260~320 行,含只读 post-unseal、只读 auth init、mount 只读加载、租约只读扫描)。
  • src/contract/vault/integration/readonly_backend.rs:仅适配错误标识,不重迁。

wrapper 层选定的 libvault::RvError 变体 = RvError::ErrString(READONLY_WRITE_DENIED)(上游唯一携带调用方可读原因、且 PartialEq 可断言的变体,../libvault-rs/src/errors.rs:366,497);具名层继续用 VaultError::ReadonlyWriteDenied / ReadonlyRuntimeTokensIncomplete,并新增 VaultError::ReadonlyStateIncomplete(承接 vendored 的 RvError::ErrCoreReadonlyStateIncomplete)与 VaultError::ReadonlyUnavailable(VLT-02 中间态桩)。

VLT-02:引用切换、只读原语搬迁、删除 vendored、迁出测试(2026-08-21)

vendored 已删除。src/vault/ 的 82 个 .rs(连同 README.md 与三份 upstream LICENSE)与 src/lib.rs 的顶层 mod vault;、以及包着它的 #[allow(...)] 块一并移除;库类型改由 crates.io 的 libvault 0.3.0 提供。rg 'crate::vault' src bin 零命中。

迁移前 迁移后
库入口 crate::vault::{RustyVault, logical::Response, storage::Backend, core::SealConfig} libvault::{RustyVault, logical::Response, storage::Backend, core::SealConfig}
RvError 本地 enum(src/common/errors/vault.rs,约 340 行) pub use libvault::errors::RvError(同文件),crate::common::errors 继续重导出
CryptoError / SealBoxError 本地 enum + crate::common::errors 重导出 不再重导出——消费方只有 vendored 的 utils/{crypto,seal}.rs,随目录一起消失;需要时直接用 libvault::utils::{crypto::CryptoError, seal::SealBoxError}
rv_error_string! / rv_error_response! / rv_error_response_status! 本地 #[macro_export] 删除(仅 vendored 使用;上游自带同名宏)
ReadonlyBackend src/vault/storage/readonly.rs src/contract/vault/integration/readonly_backend.rs
只读保险的错误标识 RvError::ErrCoreReadonlyWriteDenied(vendored 自造变体) 具名层 VaultError::ReadonlyWriteDenied;保险层 RvError::ErrString(READONLY_WRITE_DENIED)——上游唯一携带调用方可读原因且 PartialEq 可断言的变体
「状态缺失/旧格式」标识 RvError::ErrCoreReadonlyStateIncomplete VaultError::ReadonlyStateIncomplete { detail }detail 指明是 mount 表、auth 表、token salt 还是租约格式)
FIX-04 / UN-31 测试 src/vault/{fix04_list_contract,un31_readonly}.rs src/contract/vault/integration/{fix04_list_contract,un31_readonly}.rs(钉死落点)

一律未搬迁的 vendored-only 扩展(防回流守卫覆盖):RustyVault::new_readonlyCore::new_readonlyMountsRouter::load_readonlyAuthModule::load_auth_readonlyExpirationManager::restore_readonly,以及那个「租约检查线程起了没有」的观测方法。它们是对上游类型的本地 inherent 扩展,搬进集成层只会把同一个 fork 换个地方放。守卫:rg -n 'fn new_readonly|fn load_readonly|fn load_auth_readonly|fn restore_readonly|lease_checker_started' src/contract src/common src/context bin --glob '!**/config/**' 零命中(刻意排除 src/config/——loader.rsload_readonly 是无关的既有 API)。

中间态(VLT-02 → VLT-04)VaultCore::open_readonly 与库级 open_readonly_core 都是具名 fail-closed 桩,返回 VaultError::ReadonlyUnavailable。刻意不回退到可写路径——那条路径会修补掉只读打开正要保全的那份状态,「成功」的审计会是在报告一份被自己引导过程改写过的 vault。受影响用例全部 #[ignore = "VLT-04"]不得删除:迁后 un31_readonly.rs 十二条、src/context/un43_readonly_assembly.rsun43_a_needed_vault_is_opened_read_only_and_unchangedbin/tests/integration_authz_audit.rsintegration_readonly_assembly_changes_nothing。本卡为 REL-VLT-RO 家族子卡,不单独推送

回归观测点(VLT-S1 额外发现之一):vendored 在 policy_store.rs 里有一处 token policy 查询回落到持久 ACL view 的改动,那是 FIX-04 之前 JupiterBackend::list 契约发散留下的绕行。删除 vendored 即等于撤销该绕行,因此「限权 token 在重启后仍能通过 ACL 校验」必须由既有测试证明,而不是假定:fix04_an_expired_lease_is_revoked_after_a_restart(真实 Postgres 后端上 restore() 确实恢复到租约并撤销)、integration_vaultintegration_authz_audit 与 config/generic token 隔离矩阵全绿即为判据。

VLT-04:UN-31 集成层等价重建(2026-08-21)

只读引导已在集成层重建,八条 AC 逐条等价。生产落点两个文件:src/contract/vault/integration/vault_core.rs(shadow-unseal 序列)与 readonly_backend.rs(仅错误标识适配,未重迁)。全部 #[ignore = "VLT-04"] 已解除,十二条 un31_ 与四条 un43_ 全绿。

入口分层

函数 职责
生产 VaultCore::open_readonly(vault_storage, key_path) AC4:storage 已初始化、key 文件存在、份额足够、runtime_tokens 完整——任一不满足都是报告而不是修补;不调 ensure_runtime_credentials、不回写 key 文件、不撤销 root token
库级 open_readonly_core(backend, config, keys) = readonly_vault + readonly_unseal 拆开是为了让 UN-31 用例能在内存 backend 上驱动它——发生了的修补在那里表现为「多出/改变的字节」
保险 ReadonlyBackend AC6:put/delete 硬失败并计数

shadow-unseal 序列(对照上游私有 Core::post_unseal

  1. readonly_vaultRustyVault::new(backend, cfg),其中 cfg.mounts_monitor_interval 强制置 0。AC5 前半——「有没有 mounts monitor」是模式的决定,不是调用方配置的决定;un31_a_readonly_open_starts_no_background_worker 的可写对照特意配了非零 interval,所以该断言说的是模式而不是配置。
  2. readonly_unsealbarrier.inited()seal_config()弃用分片检查(与常规 unseal 一致,读到一个被轮换掉的分片不是只读该做的让步)→ 自行 ShamirSecret::combine(阈值为 1 时直接取分片)→ barrier.unseal(kek)。绕开 Core::unseal 正是因为它会调私有的 post_unseal
  3. 装配 CoreStatehmac_key / system_view / sealed = false。私有的 kekunseal_key_shares 不装——它们只服务 generate_unseal_keys(),那是写路径。
  4. readonly_post_unsealmodule_manager.setup()(纯注册)→ 只读加载 core mount 表(AC1)→ mounts_router.setup() → 只读 auth init(AC2/AC3/AC5)→ 只读 policy init(跳过 setup_policy,AC3)。

AC 与观测面

AC 实现 直接观测面
AC1 load_mount_table_readonly:只 MountTable::load;表缺失(ErrConfigLoadFailed)或存在 mount_update 会回写的条目 → VaultError::ReadonlyStateIncomplete { detail } 具名错误 + storage 快照逐字节相等
AC2 同一函数作用于 auth.mounts_router.mounts,不走 AuthModule::load_auth 同上
AC3 跳过 PolicyModule::setup_policy;token salt 在 TokenStore::new 之前预读,缺失即具名 fail-closed(新签 salt 会静默改变该 vault 里每个 token 的哈希方式) 具名错误 + denied_writes() == 0(保险层从未触发,说明拒绝发生在写之前)
AC4 见上表生产层 VaultError::{ReadonlyNotInitialized, CoreKeyTooFewShares, ReadonlyRuntimeTokensIncomplete} + key 文件字节不变
AC5 不运行 AuthModule::init(库内唯一启动过期租约 worker 的地方,也是唯一写 auth.expiration 的地方);租约只读扫描,旧格式具名 fail-closed,不注册进内存队列——队列只服务那个不存在的线程 core.mounts_monitor.load().is_none() auth.expiration.load().is_none();另有源码级零命中守卫:rg 'start_check_expired_lease_entries' src bin
AC6 ReadonlyBackend::{put,delete}RvError::ErrString(READONLY_WRITE_DENIED) 并计数 denied_writes()
AC7 保险层与 VaultCore 具名层各一组硬失败 具名层拒绝时 denied_writes() == 0
AC8 只读扫描不撤销、不启 worker 成对:可写侧 1.5s 内确实撤销并删除记录,只读侧记录仍在且零写入尝试

AC5 观测面的替换说明:vendored 曾在 ExpirationManager 上挂一个「检查线程起过没有」的标志,上游没有,且它 spawn 的线程是 detached、无句柄可问。替代面是 AuthModule.expiration——上游只有 AuthModule::init 会写它,且库内没有任何读者,而 AuthModule::init 又是唯一启动 worker 的地方;因此「槽是空的」就是「worker 没起过」这句话本身,而不是从「没观察到撤销」倒推(后者只是和 200ms tick 赛跑)。等价关系由上述源码级零命中守卫加固。

上游私有面的镜像(脆弱点,随 libvault 升级复核)

镜像 上游私有物 守卫
TOKEN_SUB_PATH / TOKEN_SALT_LOCATION 两个字面量 modules/auth/token_store.rs 的同名私有常量 un31_a_missing_token_salt_fails_closed 在同一路径上删 salt 并要求可写对照把它补回来——上游改路径会先让对照变红
StoredLease 的 serde 形状(data 必须是对象) 私有的 LeaseEntry / OldLeaseEntry 时间字段走 libvault::utils::deserialize_system_time,与库同源;un31_an_older_format_lease_fails_closed 带可写对照
只读 post-unseal 本身 私有的 Core::post_unseal 十二条 UN-31 用例 + 逐字节快照;升级 libvault 时按本节与「VLT-S1」节复核
只读 auth init 的顺序(先装 token store,后注册 policy 的 auth handler) AuthModule::set_auth_handlers 对 token store 无条件 unwrap() 顺序颠倒会让 un31_a_readonly_open_reads_what_the_writable_one_stored 直接 panic

发布节点:v0.2.68(REL-VLT-RO,2026-08-21)

plan-20260820 的家族发布点。一次推送同时带上「删除 vendored」与「只读重建」——两者不可分割:中间任一状态单独上线,要么让只读审计失效,要么让 crate::vaultlibvault 双存的语义含混。

兼容性证据:无对外 API / schema 变更。

结论
HTTP API / OpenAPI 无变化。唯一触及的 handler 是 enforce_lfs_access内部返回类型(Result<(), Response<Body>>Result<(), Box<Response<Body>>>,FIX-VLT-01),LFS 访问判定与响应字节不变
CLI 无新增/改名/删除子命令
DB schema / migration 无变化;vault 表、core_key.json 格式、seal 配置(10/5)、secret/ 挂载路径均未触碰
错误契约 MegaError 变体未变。VaultError 新增 ReadonlyStateIncomplete { detail } / ReadonlyUnavailable / ReadonlyOpen(只读引导内部条件,经 MegaError::Other 呈现,无状态码映射变化);RvError 由本地定义改为重导出 libvault::errors::RvError,变体集合以上游为准
crate surface src/lib.rsmod vault;私有模块,删除不构成对外 surface 变更(G-11 deprecation window 本项不适用)
配置项 无变化

聚合守卫(发布前全绿)

守卫 结果
rg 'crate::vault' src bin 零命中
rg 'ignore = "VLT-04"' src/contract/vault src/context bin/tests 零命中
防回流 rg 'fn new_readonly|fn load_readonly|fn load_auth_readonly|fn restore_readonly|lease_checker_started' src/contract src/common src/context bin --glob '!**/config/**' 零命中
rg 'start_check_expired_lease_entries' src bin 零命中(AC5 的等价关系加固)
un31_ 用例数 / 结果 12 / 12 passed / 0 ignored
un43_ 用例数 / 结果 4 / 4 passed
fix04_list 4 passed
三门 + cargo build / build --tests 全绿(stable 1.98.0 / nightly rustfmt)
全量 cargo test --all 973 passed / 1 ignored,与迁移前基线逐项一致

发布门期间观察到一处与本次改动无关的既有不稳定:bin/tests/** 的 docker git runner 在 cargo test --all 的并行模式下偶发失败(两次分别命中 integration_git_cli_http_round_trip / integration_git_ssh_authz_grant_immediate_effect,均停在 assert_git_success)。同一目标按 plan-template.md 规定的 -- --test-threads=1 串行跑全绿(integration_git_ssh 串行 126s vs 并行 25s,可见是共享 runner 的争用而非逻辑回归),随后的全量重跑亦全绿。按 ER-10 作瞬时错误处理(3 次尝试内收敛),不改本次发布结论;若后续复现,应作为 test-infra 独立小卡承接。

执行期发现(已记入计划修订历史)

  1. EX-03:libra 的提交签名要求 vault unseal key 可用,本环境不可用,本计划全部会推送的卡按 sign-off-only 提交(具名批准 = 计划所有者)。
  2. FIX-VLT-01:D 组 clippy 在 CI 的 stable 1.98.0 上有两条错误,本地 1.97.1 不触发——一条在 vendored pki/util.rs(随 VLT-02 删除自动消失),一条在本仓自有 src/api/router/lfs_router.rs。后者按 ER-10 另立卡并入本家族。该红在本计划开工前就存在(上一次 push 同一步骤已 failure),本次发布一并关闭。
  3. 事实基线低估:vendored↔上游另有两处非只读功能性分歧(policy_store.rs 的 ACL 持久回落、token_store.rs 的明文 token debug 日志),处置见「VLT-S1」节与 DEFER-VLT-04

D 组结论(2026-08-21):D1 config-validation = success(run 32441837770)——v0.2.67 那一轮失败的 clippy 步骤本轮已绿。D2 git-protocol-smokev0.2.68 那一轮 failure,根因是 hosted runner 镜像把预装 git 由 2.54.0 滚到 2.55.0、与工作流 pin 不符(失败于第二步,二进制尚未构建,与本次内容无关),已按 ER-04 前滚:FIX-VLT-02 上调 pin 并同步 test-infra 登记条目后,同一 workflow run 32442145005 = success

降级指引(immutable-release:已推送提交不回退。只读引导若在生产暴露缺陷,降级方式是不使用只读入口authz-audit 等只读运维路径),常规读写路径不受影响——它走 VaultCore::config,与本次改动的只读分支无交集;缺陷按 ER-10 前滚修复。

当前实现概览

vault 模块位于 src/contract/vault/,核心集成代码在 src/contract/vault/integration/

  • src/contract/vault/integration/vault_core.rs:封装 crate::vault::RustyVault(仓库内 vendored 的顶层模块),提供 VaultCoreVaultCoreInterface
  • src/contract/vault/integration/jupiter_backend.rs:将 RustyVault 的物理存储后端适配到 jupiter 数据库存储。
  • src/jupiter/storage/vault_storage.rs:通过 SeaORM 读写 vault 表,提供 list_keysloadsavedeletedelete_all
  • src/context/mod.rs:在 AppContext::new 中构造 Storage、Redis 连接、VaultCore,随后在 vault 之后启动 mail/notification dispatcher。
  • src/server/ssh_server.rs:通过 vault 保存或读取 ssh_server_key
  • src/contract/vault/pgp.rssrc/contract/vault/nostr.rssrc/contract/vault/pki.rs:基于 VaultCore 扩展 PGP、Nostr、PKI 能力。

当前 vault 已经是可用的通用 KV secret store。VaultCoreInterface 提供:

  • read_secret(name):读取 secret/<name> 下的 KV 数据。
  • write_secret(name, data):写入 secret/<name>
  • delete_secret(name):删除 secret/<name>

因此,后续把可迁移凭据写入 vault 时,不需要重新发明 secret 存储能力。真正需要补齐的是安全加固、初始化语义、错误模型、最小 bootstrap、运维命令和配置侧的 SecretRef resolver。

新增运维命令:config vault reset --force 调用 VaultCore::reset(),先删除 vault 表全部数据,再将已有的 core_key.json 备份为带时间戳的 .json.bak.<timestamp>,最后重新初始化 RustyVault 并签发新的限权 token。该命令是破坏性操作,必须通过 --force 显式确认,普通启动和服务启动不再隐式触发清库。config vault backup <DESTINATION> [--key-path <PATH>]config vault restore <SOURCE> --force [--key-path <PATH>] 已补齐 key material 备份/恢复路径:backup 复制 core_key.json 到目标位置并写入 .meta.json 元数据;restore 在临时文件上验证备份 key 能解封当前数据库后原子替换 core_key.json,避免 key 丢失场景下无法恢复。

当前实现状态速览表(2026-06-17)

能力 / 组件 实现状态 关键事实与风险
RustyVault 集成 已实现 VaultCore 封装 src/vault 中 vendored 的 RustyVault 模块,通过 JupiterBackend 落到 DB。
KV secret 读写删 已实现并收窄 read_secret / write_secret / delete_secret 使用相对 SecretName,拒绝 /secret/、空段和 ..
vault 物理后端 已拆边界 JupiterBackend 依赖 VaultBackendStorage,生产由 VaultStorage 适配;可 DB-only 构造 VaultCore
core_key.json 自动解封 已加固 只保存 unseal 分片和限权 runtime tokens,不再长期保存 root_token;缺 key fail-closed,不清库。
显式 vault reset 命令 已实现 config vault reset --force 删除 vault 表、备份 core_key.json、重新初始化;普通启动不再隐式清库。
unseal 分片 rekey 命令 已实现 config vault rekey --force [--key-path] 经最小 bootstrap 调用 VaultCore::rekey_unseal_shares() 重写 core_key.json 分片、保留数据;显式提示旧分片仍可解封、彻底失效需 KEK 轮换(阶段 J 第 1 项)。
root token 生命周期 已接入最小权限 初始化后安装 mega2 ACL policy、签发限权 token、撤销 root token;常规 secret 路径不持有 root token;generic token 显式拒绝 secret/config/*,并有 config/generic token 隔离单测。
secret 访问审计 已接 hook,可配置且默认开启,失败策略已记录,含 caller 身份 VaultCoreInterface read/write/delete 记录 vault_audit 事件(含 operationsecret_nameoutcomecaller),不包含 secret 值、root token 或分片;audit_secret_access 已 doc-comment 显式记录 fail-open 策略(审计经 infallible 的 tracing,绝不阻断 secret 操作);caller 身份经 tokio::task_local!with_audit_caller)由入口点注入(config secret set/check/rotateconfig validate --resolve-secrets、startup/reload 的 mail.password_ref 解析),未注入时为 "unknown"config.vault.audit.enabled(默认 true,经 VaultCore::with_audit_config 注入)可显式 opt-out。(2026-06-27)可配置审计 sink 已落地config.vault.audit.sink 支持 tracing(默认,infallible)与 filefile_path 指定的持久化 append-only JSONL,每条 sync_all fsync,独立于进程日志管道),并新增 config.vault.audit.fail_closed:当可失败 sink(file)写入失败时,true 让 secret 操作随之失败(non-repudiation over availability),默认 false(fail-open,仅告警不阻断)。audit_secret_access 已返回 Result,read/write/delete 在操作本身成功时才把 fail-closed 审计错误上抛。文件记录只含 ts/operation/secret_name/outcome/caller,绝不含 secret 值。
最小 DB/Vault bootstrap 已实现 VaultCore::from_database_config/from_database_connectionconfig secret set/check 只依赖数据库和 vault key。
LoadMode / SecretRef / resolver 已实现 CLI 按命令选择加载级别;SecretRef/resolver 支持 mail.password_ref 延迟解析和缓存/evict。

硬约束与不可违反的原则

本文档中以下约束是硬边界,任何实现偏离都必须重新评审:

  1. fail-closed 以 inited() 为准,绝不在 key 缺失时清库。 DB 已初始化但 core_key.json 缺失时必须返回错误(VaultError::CoreKeyMissing,见 vault_core.rs),绝不调用 delete_all()——否则会静默销毁全部已存 secret;空 DB 无 key 仍允许首次初始化。
  2. root token、unseal 分片、secret 明文绝不被主动写入日志/输出。 这些材料一旦泄露即等于 vault 失守;core_key.json 只持久化 unseal 分片与限权 runtime token,不再长期保存 root token(初始化后即撤销)。vault_lifecycle_never_logs_root_token_shares_or_secret_values 回归测试在初始化任务线程的 tracing 路径上守卫这一点;stdout/log:: facade/vault 后台线程的捕获留白见「风险与约束」。
  3. 常规 secret 访问必须使用限权 token,不得使用 root token。 初始化时安装 ACL policy 并签发 ssh/pgp/nostr/pki/config/generic 限权 token;config token 与 generic token 必须 ACL 隔离(config token 不能读 generic secret,反之亦然),有矩阵测试。
  4. vault 运维命令必须使用最小 DB/Vault bootstrap。 config secret set/checkconfig vault reset/rekey/backup/restore 只依赖数据库与 vault key 操作 secret;config validate --resolve-secrets 会解析并校验完整配置(含 Redis、对象存储等字段的校验规则)并经 vault 解析 secret。它们都不得初始化 Redis 连接、对象存储后端、完整 Storage 或 HTTP 服务(不构造完整 AppContext);缺数据库/vault 时给出明确前置提示而非 panic。
  5. 自动解封材料是静态保护边界,不能用“放进 vault”替代部署侧托管。 自动解封所需的 unseal 分片仍落在本地 core_key.json,能读取该文件的攻击者即可解封。生产高敏感部署必须配套 KMS/secret manager、受控挂载(Unix 0700/0600)、备份恢复与恢复演练;备份恢复运行手册必须与 fail-closed 配套(否则 key 丢失从“自动重建”变成“无法恢复”)。
  6. KEK 轮换无 libvault 内建原语,不在本计划承诺。 init() 后 KEK 不可变;unseal 分片 rekey(rekey_unseal_shares)可用,但彻底使旧分片失效需 KEK 轮换,须另立专项。
  7. 任何源码阶段都必须通过三项 gate。 cargo +nightly fmt --all --checkcargo clippy --all-targets --all-features -- -D warningssource .env.test && cargo test --all 全绿方可交付;禁止 blanket #[allow] 掩盖。

现状与目标对比

维度 当前状态 目标状态 实现难度
错误模型 VaultCore::new/config 已 Result 化,消费端(context/ssh/pgp/nostr/pki)错误传播 持续保持无 panic 初始化路径 中等
fail-closed DB inited 但 key 缺失返回错误、绝不清库;空 DB 可首次初始化 已配套 config vault backup/restore 运维命令与恢复运行手册,覆盖 key 丢失场景 中等
敏感输出脱敏 root token/分片/secret 明文不进 tracing 日志,有回归测试守卫 扩展捕获边界(stdout/log:: facade/后台线程)需全局 subscriber 或 fd 捕获 复杂
物理后端边界 JupiterBackend 依赖 VaultBackendStorage trait,可 DB-only 构造 维持窄接口,避免重新耦合完整 Storage 简单
接口收窄 read/write/delete_secret 用校验过的 SecretName,raw/token API 已 pub(in crate::contract::vault) 收窄 持续防止 raw API 外泄 简单
最小 bootstrap from_database_config/connection + config secret/vault 命令已落地 维持运维命令不依赖完整 AppContext 简单
权限与 root token 初始化签发限权 token、撤销 root;config/generic ACL 隔离有矩阵测试 保持迁移更多生产凭据前 root 已退役 中等
审计 read/write/delete 记录 vault_audit(operation/secret_name/outcome/caller),sink 支持 tracing/file,可 fail_closed,默认开启 远程/HTTP sink 为后续 中等
SecretRef 消费 mail.password_ref、notification slack/webhook、对象存储凭据 SecretRef 已落地,post-vault 解析 渠道凭据运行期热加载(mail 已支持,slack/webhook 为后续) 中等
轮换与 rekey config vault rekey 重写 unseal 分片;config secret rotate 覆写可迁移 secret KEK 轮换需专项(无 libvault 原语) 复杂

启动依赖顺序

当前服务启动的关键顺序如下:

Config::new
  -> database_connection()                                    # 建数据库连接
  -> VaultCore::from_database_connection(db, key_path)        # DB-only vault 初始化
  -> resolve_object_storage_secrets(object_storage, vault)    # 解析对象存储 vault:// SecretRef
  -> build_object_storage(resolved_config)                    # 构造对象存储后端
  -> Storage::new_with_connection(config, db, object_store)   # 复用同一连接建完整 Storage
  -> init_connection(redis)                                   # 连接 Redis
  -> SmtpMailer + EmailDispatcher                             # mail 启用时,vault 之后启动
  -> init_monorepo
  -> HTTP / SSH / multi 服务分发

该顺序形成硬约束:

  • database.db_url / 数据库密码属于引导配置,不能进入本项目 vault。
  • object_storage.s3.access_key_id / secret_access_key 在 DB-only vault bootstrap 后解析,已支持 vault:// SecretRef;namespace 为 vault://secret/config/<profile>/object_storage/access_key_id#<field>.../object_storage/secret_access_key#<field>,且已在 config validate / config secret set/check / --resolve-secrets 中对齐。
  • redis.url 在 vault 就绪后连接,因此从启动顺序上已具备迁移条件;已支持 vault:// SecretRef(2026-06-28),由 AppContext::new 中的 resolve_redis_url_secret 解析,合法 namespace 为 vault://secret/config/<profile>/redis/url#<field>,且已在 config validate / config secret set/check / --resolve-secrets 中对齐;字面量 URL 原样透传。
  • mail.password 的消费晚于 vault 就绪;当前在 AppContext::new 中构造 SmtpMailer 并启动 EmailDispatcher,是第一批较合理的可迁移凭据。
  • config secret set/checkconfig validate --resolve-secrets 不能复用完整 AppContext,必须使用最小 DB/Vault bootstrap。

任何试图在 Config::new 中读取 vault secret 的方案都不可行,因为 Config::new 是同步加载阶段,且此时 vault 还没有就绪。

Vault 初始化时序(按代码核实)

事实校准(2026-06-24):本节为阶段 A 加固前的代码级分析快照,其中描述的“key 缺失即 delete_all 清库”、“println!/log::debug! 输出 root token”、“assert! 触发 unseal”等行为已在阶段 A 消除(见本文件顶部“落地状态更新”与阶段 A 工作项)。当前 VaultCore::config fail-closed、不再输出 root token、普通启动不再隐式清库。本节保留以记录历史决策脉络,新的实现以代码与阶段 A 描述为准。

本节是上面“启动依赖顺序”的代码级展开,所有步骤都标注了文件与行号,供实现与评审核对。启动分为两个阶段:同步阶段(尚未创建 tokio runtime,vault 不可能就绪)与异步阶段service::exec#[tokio::main] 启动 runtime 之后)。

进程入口到 vault 就绪

【同步阶段 · 无 runtime】
main()                                          src/main.rs:34
└─ cli::parse(None)                             src/cli.rs:21
   ├─ ConfigLoader::new(input).load()           src/cli.rs:35   解析配置路径
   ├─ Config::new(path)                         src/cli.rs:37   同步加载 TOML(vault 未就绪)
   ├─ init_log(&config.log)                     src/cli.rs:44   ★ 安装 tracing subscriber(早于 vault)
   ├─ ctrlc::set_handler                         src/cli.rs:52
   └─ exec_subcommand(config, "service", ..)    src/cli.rs:65 → builtin_exec → service::exec

【异步阶段 · #[tokio::main] 启动 runtime】       src/commands/service/mod.rs:24
service::exec(config, args)
└─ AppContext::new(config).await               src/context/mod.rs:24
   ├─ database_connection(&database)            src/context/mod.rs:56           建 DB 连接
   ├─ VaultCore::from_database_connection(..)   src/context/mod.rs:60-71  ◀── DB-only vault 在此初始化
   ├─ resolve_object_storage_secrets(..)        src/context/mod.rs:75-76        vault 后解析对象存储 SecretRef
   ├─ build_object_storage(..)                  src/context/mod.rs:77-78        ★ 对象存储(vault 后)
   ├─ Storage::new_with_connection(..)          src/context/mod.rs:79           复用同一连接建完整 Storage
   ├─ init_connection(&config.redis)            src/context/mod.rs:80-81        Redis(vault 后)
   ├─ SmtpMailer + NotificationService spawn    src/context/mod.rs:82-230       (vault 之后,mail 启用时)
   └─ mono_service.init_monorepo(&monorepo)     src/context/mod.rs:231-232      (vault 之后)
└─ 分发 http::exec / ssh::exec / multi::exec    src/commands/service/mod.rs:34-36
      └─(SSH 路径)读/生成 ssh_server_key       src/server/ssh_server.rs:78     ← vault 后续消费者之一

Vault 内部时序(VaultCore::newconfig

VaultCore::new(storage)                          src/contract/vault/integration/vault_core.rs:52
├─ dir = mega_base()/vault ; create_dir_all      :53,56   (expect;无权限收紧)
└─ VaultCore::config(storage, key_path)          :60
   ├─ backend = JupiterBackend::new(storage)      :61   (RustyVault 物理后端 = DB 的 vault 表)
   ├─ SealConfig { secret_shares:10, secret_threshold:5 }  :62
   ├─ RustyVault::new(backend, None)              :67   (expect)
   ├─ if !key_path.exists():                      :70   ── 分支 A:首启 / key 缺失
   │    ├─ println!("clearing database…")         :71
   │    ├─ vault_storage.delete_all()             :74   ⚠ 清空 vault 表(数据丢失路径)
   │    ├─ rvault.init(&seal_config)              :79   → root_token + 10 个 shares
   │    ├─ println!("root token: {}", …)          :83   ⚠ root token 进 stdout
   │    └─ File::create(core_key.json) + 写入      :98   ⚠ 默认 umask 权限
   │  else:                                       :102  ── 分支 B:key 存在
   │    └─ read + 反序列化 CoreKey{shares,token}  :104
   ├─ for i in 0..5 { rvault.unseal(shares[i]) }  :108  解封(assert! ok)
   ├─ log::debug!("root token: {}", …)            :114  ⚠ root token 进 tracing(debug 级)
   └─ return VaultCore{ rvault, key }             :122  ◀── vault 就绪

时序图

sequenceDiagram
    autonumber
    participant M as main()
    participant CLI as cli::parse (同步)
    participant SVC as service::exec #[tokio::main]
    participant AC as AppContext::new
    participant DB as Database
    participant VC as VaultCore
    participant RV as RustyVault
    participant VDB as vault 表 (JupiterBackend)
    participant OBJ as ObjectStorage
    participant ST as Storage::new_with_connection
    participant RDS as Redis
    participant MAIL as Mail/Notification
    participant SRV as http/ssh 服务

    M->>CLI: parse(None)
    CLI->>CLI: ConfigLoader.load() + Config::new()(vault 未就绪)
    CLI->>CLI: init_log() ★ tracing 安装
    CLI->>SVC: exec_subcommand → service::exec(runtime 启动)
    SVC->>AC: AppContext::new(config).await
    AC->>DB: database_connection() 建 DB 连接
    AC->>VC: VaultCore::from_database_connection(db, key_path) ◀ DB-only vault 初始化
    AC->>VC: resolve_object_storage_secrets(config.object_storage, vault) 解析 vault://
    AC->>OBJ: build_object_storage(resolved_config)
    AC->>ST: Storage::new_with_connection(config, db, object_store)
    AC->>RDS: init_connection(redis)
    VC->>VC: create_dir_all(base/vault)
    VC->>RV: RustyVault::new(JupiterBackend)
    alt core_key.json 不存在(首启/丢失)
        VC->>DB: delete_all() ⚠ 清空 vault 表
        VC->>RV: init(Seal{10,5}) → root_token + 10 shares
        VC-->>SVC: println!/log::debug! 输出 root token ⚠
        VC->>VC: 写 core_key.json(默认权限 ⚠)
    else core_key.json 存在
        VC->>VC: 读取并反序列化 CoreKey
    end
    loop 5 次 (secret_threshold)
        VC->>RV: unseal(shares[i]) assert ok
    end
    VC-->>AC: VaultCore(vault 就绪)
    AC->>MAIL: SmtpMailer::new + EmailDispatcher::new + spawn(mail 启用时)
    AC->>ST: mono_service.init_monorepo()(vault 之后)
    AC-->>SVC: AppContext
    SVC->>SRV: http::exec / ssh::exec / multi::exec
    SRV->>VC: (SSH) read_secret("ssh_server_key")|不存在则生成并 write_secret
Loading

时序中的关键事实

  • vault 就绪点是唯一的 VaultCore::from_database_connectioncontext/mod.rs:60-71)。在它之前只消费 DB 连接;对象存储凭据在 vault 就绪后通过 resolve_object_storage_secretscontext/mod.rs:75-76)解析,对象存储(build_object_storagecontext/mod.rs:77-79)与完整 Storage::new_with_connection:81-86)、Redis(:88)均位于 vault 之后。
  • mail.password 的消费点(mailer_from_config + NotificationService::from_mail_config_with_extra_channels + tokio::spawn(service.start))在 context/mod.rs:95-231,晚于 vault,是第一批可迁移凭据。当前失败路径返回可诊断错误,不再静默忽略。
  • init_monorepo 在 mail/notification dispatcher 启动之后、服务分发之前执行(context/mod.rs:233)。
  • tracing subscriber 在 cli.rs:44 就已安装,早于 vault;因此 VaultCore::configprintln!(:71/:83/:93/:103)与 log::debug!(root_token)(:114)会真的把 root token / 分片写进 stdout 与日志(详见“当前主要问题 · root token 明文输出”)。
  • 分支 A 的 delete_all()(:74)仅凭 core_key.json 不存在即触发,无法区分“全新空库首启”与“误删 key 但库内有数据”(详见“fail-closed 判定依据必须区分首次初始化与误删 key”)。

威胁模型与安全边界

本计划的所有加固动作都应服务于一个明确的威胁模型;缺少威胁模型时,“是否符合 Vault 安全标准”无法被验证。

资产:

  • vault 加密主密钥与 unseal 分片(core_key.json 中的 secret_shares)。
  • vault root token。
  • vault 内业务 secret(SSH host key、PGP / Nostr / PKI 私钥,以及未来迁移的 mail.password 等)。

假定的攻击者与防护目标:

  • 仓库与代码评审泄露:secret 不得进入 git、TOML、镜像层。可防护。
  • 日志与可观测性泄露:root token、分片、secret 明文不得进入 stdout / stderr / tracing / 错误信息。可防护(阶段 A)。
  • 应用层误用:调用方不应接触 root token,也不应自由拼 raw path。可防护(阶段 C)。
  • 进程内存读取:同一进程内 secret 必然以明文存在,不在本计划防护范围。
  • 可读取部署机磁盘的攻击者:当前自动解封把分片与 root token 落盘,磁盘读取即等于完全攻陷 vault。当前实现不防护此攻击,只能通过外部 KMS / transit 自动解封或 systemd / K8s 注入降低,属阶段 J 与部署侧职责。

残余风险(必须在文档与运维说明中显式声明):

  • 自动解封模式下,secret_shares: 10 / secret_threshold: 5 的 Shamir 分片不提供职责分离收益——全部 10 份分片与 root token 同处一个 core_key.json,并在启动时自动用其中 5 份解封。分片在当前形态下是“安全装饰”;真正的职责分离需要把 key material 交给外部 KMS / transit seal,或多人托管分片(与无人值守启动互斥)。libvault 提供 unseal_once()core.rs:534)一次性解封原语,可在外部 / 人工解封场景下"用后即废"分片以防重放,但对"分片落盘后自动解封"这一当前形态无帮助——后者仍只能由外部 KMS / transit 缓解。
  • 因此安全收益的准确表述是“不进仓库、不进普通配置、不进日志、不被普通应用调用方接触”,而不是“可抵御获得磁盘读权限的攻击者”。

本期对齐的 Vault 标准子集:fail-closed seal、最小权限与 root token 生命周期、secret 访问审计、轮换 / rekey、脱敏、备份恢复。明确不在本期范围(如需可另立计划):动态 secret 与租约 TTL、transit 加密即服务、response wrapping、HSM 集成。

当前主要问题

core_key.json 缺失会清空 vault

src/contract/vault/integration/vault_core.rs 当前在 key 文件不存在时会:

  1. 打印清库重建提示。
  2. 调用 vault_storage.delete_all() 清空 vault 表。
  3. 重新初始化 RustyVault。
  4. 生成新的 root token 和 secret shares。
  5. 写入新的 core_key.json

这意味着 core_key.json 丢失会导致所有 vault secret 被删除。集中更多凭据后,这会变成灾难性数据丢失路径。普通启动和 config secret check 必须改为 fail-closed:key 缺失时启动失败并提示恢复或显式 reset,不能自动清库。

当前测试 test_vault_reinitialize_after_file_loss 还固化了这一危险行为,后续需要改为验证 fail-closed。

root token 明文输出

VaultCore::config 当前会通过 stdout 和 debug log 输出 root token。这违反 secret 迁移前的基本安全前提。迁移 SecretRef 前必须保证:

  • root token 不进入 stdout。
  • root token 不进入 stderr。
  • root token 不进入 tracing 日志。
  • secret shares 和完整 core_key.json 内容也不得输出。

否则,SecretRef 只能避免凭据进入 TOML,不能避免凭据进入日志。

core_key.json 权限未显式收紧

当前 core_key.json 使用 std::fs::File::create 创建,依赖系统默认 umask。生产前需要显式加固:

  • Unix 下 vault 目录权限应为 0700 或等价限制。
  • Unix 下 core_key.json 权限应为 0600
  • 容器镜像、备份、日志采集必须排除该文件。
  • 生产部署应优先考虑 systemd credential、Kubernetes Secret volume 或外部 secret manager 注入 key material,而不是长期保存普通明文文件。

即便完成权限加固,当前自动解封模式仍不能抵御能读取部署机磁盘的攻击者。安全收益应限定为“不进仓库、不进普通配置、不进日志”。

初始化路径大量 panic

VaultCore::new / VaultCore::config 当前返回 Self,内部使用大量 expectunwrapassert。这让调用方无法区分:

  • vault 目录创建失败。
  • key 文件不存在。
  • key 文件不可读。
  • key 文件格式错误。
  • RustyVault 创建失败。
  • init 失败。
  • unseal 失败。
  • 数据库存储失败。

后续应改为返回 Result<Self, MegaError>,或引入 VaultError 后统一转换为 MegaError。普通服务启动、config secret checkconfig validate --resolve-secrets 都需要可诊断错误,而不是 panic。

JupiterBackend 依赖完整 Storage

JupiterBackend 当前接收完整 Storage,但实际只使用 ctx.vault_storage()。这让 vault bootstrap 被完整 Storage::new 绑定,而 Storage::new 会提前构造对象存储等能力。

这条接口边界太粗,直接阻碍 config secret set/check 独立运行。目标是拆出最小 DB/Vault bootstrap,只建立数据库连接和 VaultStorage 所需能力,不初始化 Redis、对象存储、HTTP、SSH、monorepo 或后台任务。

VaultCoreInterface 暴露过多底层细节

当前 interface 包含 token()read_apiwrite_apidelete_apiread_secretwrite_secretdelete_secret。普通调用方不应该知道 root token,也不应该直接拼 RustyVault API path。

后续应分层:

  • vault 内部保留 raw API 能力。
  • 业务调用方只使用规范化 secret name。
  • 配置系统通过 SecretResolver 使用 SecretRef,不直接依赖 raw vault path。

secret path 语义容易误用

write_secret(name) 内部会访问 secret/{name}。如果调用方传入 secret/config/prod/mail/password,最终会访问 secret/secret/config/prod/mail/password

因此运维命令和 resolver 必须明确区分:

  • VaultCore 的相对 secret name:config/prod/mail/password
  • 配置文件里的引用:vault://secret/config/prod/mail/password#value
  • RustyVault 内部 path:secret/config/prod/mail/password

resolver 不能把完整 URI 直接传给 read_secret

消费端假设 vault 数据永远正确

早期 SSH、PGP、Nostr 等调用点大量使用 unwrap()expect(),并假设 secret JSON shape 永远正确。例如:

  • src/server/ssh_server.rs 读取 ssh_server_key.secret_key
  • src/contract/vault/pgp.rs 读取 pgp-signed-secret.pub_key / sec_key
  • src/contract/vault/nostr.rs 读取 nostr_identity_key.nostr / secret_key

当前 SSH、PGP、Nostr 的读取/生成/保存主路径已改为返回可诊断错误;PGP/Nostr 保存路径也不再通过 json().as_object().unwrap() 构造 KV map。后续仍需继续清理 PKI 和其它调用点中的测试外 panic 风格路径。

root token 永久留存,且应用全程以 root 身份操作

core_key.json 永久保存 root token,VaultCoreInterface 的所有读写都用 self.token()(即 root token)直连 RustyVault。这违反 Vault 的最小权限与 root token 生命周期标准:root token 应仅用于初始化,之后撤销,日常操作通过按 policy 限权的非 root token 完成,需要时再用 generate-root 临时重建。

仅“不输出 root token”无法解决这一点:只要 root token 长期存在并用于每次操作,一旦泄露即等于完全攻陷。阶段 C 收窄的是 Rust 调用面,并非 vault 层授权——底层仍是 root。真正的最小权限需要在 vault 内定义 policy,并为 SSH、PGP、Nostr、PKI、config 等消费端签发各自的限权 token(见阶段 I)。

缺少 secret 访问审计

当前没有任何 secret 访问审计。集中托管凭据的系统若无法回答“谁、在何时、访问了哪个 secret、结果如何”,不满足 Vault 生产加固的基本要求。需要启用审计设备(或在 interface 上统一审计 hook),记录每次 secret 读写的元数据,并对 secret 值做哈希 / 省略;审计日志本身不得泄露明文、root token 或分片(见阶段 H)。

缺少轮换、rekey 与备份恢复方案

计划目前只有破坏性 re-init,缺少三类标准能力:

  • vault 加密 key 轮换(sys/rotate 等价能力)。
  • unseal 分片 rekey:core_key.json 疑似泄露后,在不丢数据的前提下重新生成分片与 root token。
  • 业务 secret 轮换(例如周期性更换 mail.password)。

更关键的是,阶段 A 把 key 缺失改为 fail-closed 是正确的,但 fail-closed 的另一面是“缺少恢复路径就等于 key 丢失即永久数据丢失”。文档要求把 core_key.json 排除出镜像 / 日志 / 备份,却没有定义 key material 的安全托管位置与恢复流程。必须明确:分片 / root token 的安全备份位置(外部 secret manager、离线托管等)、恢复步骤,以及“DB 数据尚在但 key 丢失”时的 rekey / 恢复预案(见阶段 J)。这与 config.md 中“Vault 加固是 SecretRef 生产化迁移硬前置”的约束一致。

fail-closed 判定依据必须区分首次初始化与误删 key

仅以“core_key.json 是否存在”做判定会引入新缺陷:全新部署本来就没有 key 文件,若一律 fail-closed,则在阶段 D 的 CLI 初始化命令落地前,全新环境将无法完成首次初始化

正确判定依据是 vault 在 DB 中是否已初始化(rvault.core.load().inited(),测试已使用该接口),而非 key 文件是否存在:

  • DB 未初始化 + key 文件缺失 → 合法首次初始化,允许 init 并写入新 key 文件。
  • DB 已初始化 + key 文件缺失 → 危险(数据在、解封材料丢失)→ fail-closed,提示恢复或显式 reset,绝不 delete_all()

core_key.jsonvault 表缺少格式 / 兼容迁移

一旦阶段 A / I 改变 key 文件语义(例如不再长期保存 root token、或新增字段),已有部署的旧 core_key.json 需要兼容或迁移路径,否则升级即触发 fail-closed。计划需要为 key 文件和 vault 表的格式演进定义版本字段与迁移策略。

字段迁移分类

字段 当前消费点 分类 迁移结论
database.db_url / 数据库密码 Storage::new 建库连接 引导配置 不能进本项目 vault;通过 TOML/env/部署平台 secret 提供
redis.url AppContext::new 中 vault 后 resolve_redis_url_secretinit_connection 早期运行时依赖 已支持 vault:// SecretRef(2026-06-28);合法 namespace 为 vault://secret/config/<profile>/redis/url#<field>;字面量 URL 原样透传,含密码时日志必须脱敏
object_storage.s3.* AppContext::new 中 DB-only vault bootstrap 后 resolve_object_storage_secretsbuild_object_storage 早期运行时依赖 已支持 vault:// SecretRef;合法 namespace 为 vault://secret/config/<profile>/object_storage/access_key_id#<field>.../secret_access_key#<field>;字面量凭据原样透传
orion_server.db_url Orion 相关配置 引导或独立服务配置 不默认纳入 mega2 vault;按 Orion 启动依赖单独判断
mail.password AppContext::new 中 vault 之后构造 SmtpMailer 并启动 EmailDispatcher 可迁移凭据 第一批已改为支持 SecretRef;构造失败已从静默忽略改为可诊断处理
ssh_server_key SSH server 启动时读取或生成 vault 内部 secret 已由 vault 管理;读取、生成和写入失败已返回可诊断错误;剩余重点是部署侧 key material 托管
PGP / Nostr key vault/pgp.rsvault/nostr.rs vault 内部 secret 已由 vault 管理;读取、解析、保存和删除主路径已返回 Result,缺字段/坏格式不再 panic

跨模块协同前置(2026-06-16 修订)

以下事项不是所有 vault 工作的统一硬前置。它们只阻塞依赖对应能力的阶段;vault 本地安全止血、最小 bootstrap、interface 收窄、消费端 panic 清理可以先行推进。

现状提示(2026-06-15):经核查,下列三项前置在当前仓库均尚未实现。但它们只阻塞依赖各自前置的阶段(A 的脱敏部分、D、E、G)——存在一条不依赖它们的 vault-only 执行链(A 止血子集 → B → C → F),详见下文《可执行性评估》。

1. 日志脱敏工具(redaction 模块)设计与实现

落地状态(2026-06-19):已在 src/config/redaction.rs 落地 Redactor trait + UrlRedactor + global_redactor() + redact_db_url/redact_redis_url/redact_url,并接入真实调用点:DB 连接日志(jupiter/storage/init.rsredact_db_url)、Redis 连接日志(jupiter/redis/mod.rsredact_redis_url)、notification dispatcher 错误日志(global_redactor().redact(...) 脱敏 URL userinfo)。redact_secret/redact_json 刻意未实现——secret 值由 SecretRef/SecretString 自身脱敏(Debug/Display/Serialize 输出 ***),CoreKey/root token/分片在阶段 A 直接不输出而非先构造再脱敏,故无 redact_json 消费端;如未来需要跨模块脱敏 JSON 日志再补。下文为原始设计建议,保留供参考。

目标:建立统一的敏感信息脱敏工具,供 config、vault、mail、notification 等模块共享使用。

位置建议src/common/redaction.rssrc/config/redaction.rs(已采用 src/config/redaction.rs

职责范围

  • 脱敏 URL(移除密码和关键参数)
    • 示例:postgres://user:password@host:5432/dbpostgres://***:***@host:5432/db
  • 脱敏 token 和密钥
    • 示例:secret_abc123xyzsecret_***
  • 脱敏 Vault 相关信息
    • root token:完整隐藏
    • secret shares:完整隐藏
    • secret 值:完整隐藏(仅保留路径)
  • 脱敏配置相关信息
    • 数据库 URL 中的密码
    • Redis URL 中的密码
    • 对象存储密钥

API 设计建议

pub trait Redactor {
    /// 脱敏字符串(通用脱敏策略)
    fn redact(&self, value: &str) -> String;
    
    /// 脱敏数据库 URL
    fn redact_db_url(&self, url: &str) -> String;
    
    /// 脱敏 Redis URL
    fn redact_redis_url(&self, url: &str) -> String;
    
    /// 脱敏密钥/token(假设是不可见字符或长字符串)
    fn redact_secret(&self, value: &str) -> String;
    
    /// 脱敏 JSON(递归处理,隐藏 password、token、secret 等关键字段)
    fn redact_json(&self, json_str: &str) -> String;
}

pub fn global_redactor() -> &'static dyn Redactor;

在 vault 中的使用点

  • 需要保留但可能包含敏感上下文的日志 / 错误输出:使用 redactor 后再输出;阶段 A 的 root token / 分片 / key 文件内容应直接删除输出,不应先构造再脱敏
  • 错误信息中:使用 redactor.redact_json() 处理 CoreKey 反序列化失败的错误信息
  • VaultErrorDisplay 实现中:所有敏感字段都应经过脱敏

前置条件

  • 只阻塞“需要保留但必须脱敏的日志/错误输出”、跨模块脱敏测试与 SecretRef 生产化 gate。
  • 不阻塞阶段 A 中“删除 root token / 分片 / key 文件内容输出”的核心止血;这部分应直接删除敏感输出,避免先构造再脱敏。
  • 可作为独立前置工作先完成
  • 不被任何其他阶段阻塞

验收标准

  • 脱敏工具能被 config、vault、mail、notification 等模块导入使用
  • 单元测试覆盖所有脱敏类型
  • 脱敏后的输出不包含明文敏感信息(token、密码、shares 等)

2. CLI 两阶段加载框架(与 config 团队协同)

目标:与 config.md 阶段 2 协同设计 LoadMode 框架,为两个模块都需要的 CLI 改造提供统一的设计。

设计范围

  • 定义 LoadMode enum(所有可能的加载级别)
  • 明确各模式的启动路径和依赖
  • 定义命令与加载模式的映射关系

期望产出

  • 共同设计文档或 RFC
  • src/cli/load_mode.rs 或等价的设计代码框架

前置条件

  • 阶段 D(P1)需要与 config 阶段 2 协同完成此设计
  • 不能分别实施,否则会出现不一致

3. 与 config.md 的进度协调

关键约束

  • 阶段 B(最小 bootstrap)应在 config 阶段 3 之前完成,或至少同期进行
  • config 阶段 3 直接依赖 vault B 的改造结果
  • 两个团队应协商明确的交付顺序和时间表,避免 config 因等待 vault 而延期

可执行性评估(2026-06-15 核查;2026-06-16 补充验证通过,详见文首"落地可行性分析补充")

本节按"当前代码与依赖的实际状态"核查各阶段能否立即执行,仅调整可行性与排期,不改动代码。核查基于 mega2 当前 src/src/vault 中 vendored 的 libvault

可执行度总评:中高(7/10)。 vault 自身的 P0/P1 加固路径具备落地条件,主要障碍不是技术不可行,而是文档后半部分仍沿用旧前置口径:把"删除敏感输出"错误地写成必须等待 redaction,把"最小 bootstrap"与"LoadMode/SecretRef"混在同一阻塞链里。执行时必须把工作拆成两条队列:

  • vault-only 队列:A 核心止血 → B → C → F,可立即开工;H、I 在 C 之后接入,不需要等待 LoadModeSecretRef
  • 跨 config 队列:redaction、LoadModeSecretRef、对象存储后置初始化,按 config.md 对应阶段协同推进;这些工作不应反向阻塞 A 核心。

可执行性问题诊断

  1. 前置依赖口径需保持收敛:A 核心不依赖 redaction;文档中任何把“删除敏感输出”写成“必须等待脱敏工具”的表述都应删除或降级为“保留输出时才需要脱敏”。否则会让可以立即落地的安全止血被误判为阻塞。
  2. 阶段颗粒度不够执行化:A 同时包含删除输出、错误模型、fail-closed、权限、reset 命令设计,实际应拆成"A1 直接止血"、"A2 初始化语义"、"A3 权限与测试"三个可评审单元。
  3. 跨模块工作与本地工作混排:D/E/G 的确依赖 config,但 B/C/F/H/I 不依赖 config 交付,优先级表应显式分流。
  4. 验收标准可测,但缺少入口清单:已有验收项大多具体,但缺少"第一批 PR 改哪些文件、不能改哪些范围"的执行边界。
  5. 仓库格式容易误判:当前工作副本使用 Libra 管理,git diff 失败不是代码或文档不可执行的证据。执行者应先用 libra status 确认已有改动,再用 libra diff -- docs/refactoring/vault.md 或对应路径核对差异,避免误操作其他人已有改动。
  6. 代码定位应以符号为准:当前行号有轻微漂移,执行时应以 VaultCore::configJupiterBackend::newVaultCoreInterface 等符号定位,行号只作辅助。

关键事实(已核查)

  1. 三项跨模块"硬前置"在当前仓库尚不存在

    • 日志脱敏工具(config 0b):src/common/redaction.rs / src/config/redaction.rs 不存在,无 Redactor
    • CLI LoadMode(config 2):src/cli/load_mode.rs 不存在,无 LoadMode
    • config secret set/check/refSecretRefconfig validate --resolve-secrets:未实现。
    • 凡声明依赖这三者的阶段(A 的脱敏部分、D、E、G),若不先交付前置,无法照原文执行。
  2. libvault 内建治理路径开箱可达(embedded API:init()unseal() 后用 root token 即可 write/read):sys/policy/{name}sys/policies/acl/{name}auth/token/createsecret/* 均为默认挂载(src/vault/mount.rs:45-64modules/auth/mod.rs:38-48core.rs:609-632post_unseal)。→ 阶段 I 的 policy/token 不需要额外挂载 plumbing,mega2 侧即可落地。阶段 H 不依赖这些内建路径,应在 mega2 的收窄 interface 上实现 hook。

  3. fail-closed 判定依赖的 rvault.core.load().inited() 存在且测试已用——阶段 A 第 5 项可直接执行。

  4. 当前工作副本是 Libra 格式仓库;工作区检查应使用 libra status,差异检查应使用 libra diff -- <path>。这只影响开发/评审工作流,不改变 vault 代码的技术可执行性。

  5. 行号轻微漂移:本次迁移合并了 context/mod.rs 的 mail 分支(原文 :46-55 现约 :48-57),vault_core.rs 多数行号仅 ±1。执行前应以符号(函数名 / 路径字符串)而非行号定位

可执行性分级

阶段 可执行性 依据 / 调整
A(止血)核心子集 ✅ 现在可执行,无外部依赖 "停止输出 root token / 分片 / key 文件内容"只需删除 println!/log::debug! 并停止构造含密字符串——不需要脱敏工具。fail-closed(inited())、文件权限(0700/0600)、Result/VaultError 均为本地改造。
A 中"脱敏残留日志"部分 ⛔ 阻塞于 config 0b 仅当需把含密 URL(DB/Redis)部分脱敏后仍输出时才需要;与"止血"解耦,不应连坐阻塞 A 的核心。
B(最小 bootstrap) ✅ 现在可执行 纯 mega2 结构改造。
C(收窄 interface) ✅ 现在可执行 纯 mega2 改造;阶段 H 的审计 hook 建议并入本阶段产物。
F(清理消费端 panic) ✅ 可执行(依赖 A/B) PKI 路径已迁移,错误模型清理基于新路径。
H(审计 hook) 🟡 可执行,需先定审计落点与失败策略 只走 interface hook(内建审计是桩);技术无阻塞,排在 C 之后。
I(policy + 非 root token) ✅ 已实现 已安装 mega2 runtime ACL policy、签发限权 token、撤销 root token;同时修复 libvault token policy cache 重启 fallback 与明文 token debug 日志。
J:分片 rekey / secret 轮换 🟡 可执行(有内建原语) 基于 generate_unseal_keys() / unseal_once()
D(CLI LoadMode + secret 命令) ✅ 已实现 LoadModeconfig secret ref/set/checkconfig validate --resolve-secrets 已落地;vault 命令使用最小 DB/Vault bootstrap。
E(SecretRef + mail.password_ref) ✅ 已实现 SecretRef / resolver / mail.password_ref 已落地,明文 passwordpassword_ref 互斥。
G(对象存储后置) ✅ 边界关闭 未迁移对象存储凭据,故不做完整 Storage 后置重排;object_storage.* 仍不得使用本项目 Vault SecretRef
J:KEK 轮换 ⛔ 无内建原语 须自建,另立专项。

可行性改进结论

  • 存在一条完全不依赖 config 团队的 vault-only 执行链:A(止血子集)→ B → C → F,可立即开工;H、I 紧随其后(libvault 已确认可达)。这条链不应被尚不存在的脱敏工具 / LoadMode 连坐阻塞。
  • 解除阶段 A 的伪硬依赖:把"停止输出敏感值"(现在可做)与"脱敏需保留的日志"(待 config 0b)拆为两个独立工作项,前者不再等待脱敏工具。
  • D / E / G 仍是真阻塞:依赖 config 团队的 LoadMode / SecretRef / 对象存储重排;在它们就绪前,不要把 mail.password_ref 迁移列入"可执行"范围。
  • 仓库格式不构成技术阻塞:Libra 管理工作副本只改变状态/差异/提交命令;本计划的代码入口、测试命令和风险判断仍按 mega2 源码结构执行。
  • 执行前以符号定位而非文档行号(迁移已造成轻微漂移)。

建议执行切片

为了避免一个 PR 同时触碰初始化语义、CLI、配置解析和业务消费端,建议按以下可回滚切片推进:

  1. A1 直接止血:删除 root token / 分片 / key 文件内容输出,避免新增脱敏依赖;只改初始化路径和对应测试。
  2. A2 初始化语义VaultCore::new/config 返回 Result,fail-closed 以 DB 初始化状态为准,移除普通启动中的 delete_all() 数据丢失路径。注意:此步需同步处理 AppContext::new 及直接调用点的错误传播(属于允许的最小波及,见"下一步建议"执行边界)。
  3. A3 key material 权限:Unix 权限 0700/0600 与权限断言测试;非 Unix 平台只验证不放宽现有行为。
  4. B 最小 bootstrap:拆 JupiterBackend 的 storage 边界,但不新增 config secret 命令。
  5. C/H interface 与审计入口:先隐藏 token/raw path,再挂审计 hook;审计目的地与 fail-open/fail-closed 策略须在实现前明确。
  6. F 消费端错误模型:SSH、PGP、Nostr、PKI 分批清理 panic,不与 SecretRef 迁移混在同一 PR。

暂不可执行边界

  • 不实现 mail.password_ref,直到 SecretRef / resolver 与 mail 消费形态就绪。
  • 不实现 config secret set/check,直到 LoadMode 与最小 bootstrap 都就绪。
  • 不把 DB、Redis、S3 凭据迁入本项目 vault,除非先完成对应启动顺序重排。
  • 不承诺 KEK 轮换,除非另立 libvault 数据重加密专项。
  • (2026-06-16 分析确认)首批落地切片仅限 vault 相关文件 + 必要的 context/命令调用方最小适配;严禁在 A1-A3 阶段混入 SecretRef、LoadMode、redaction 实现或 config 命令。所有代码变更必须先通过 AGENTS.md 三大门禁验证。

改进原则

  1. 先加固 vault,再迁移配置 secret。
  2. Config::new 只解析 SecretRef,不读取真实 secret。
  3. secret 真实值解析必须发生在 vault 就绪之后。
  4. config secret set/check 必须使用最小 DB/Vault bootstrap。
  5. 引导配置和早期运行时依赖不进入本项目 vault。
  6. root token、secret shares、明文 secret、数据库 URL 密码、Redis URL 密码都不得进入日志或错误信息。
  7. 破坏性 reset 必须是显式运维动作,不能隐藏在普通启动中。
  8. fail-closed 必须与备份恢复方案配套;引入 fail-closed 前后,key 丢失(数据在)必须有文档化的恢复或安全重置路径。
  9. 最小权限优先:root token 仅用于初始化,常规运行通过限权 token;调用方不接触 root token,也不直接拼 raw path。
  10. secret 访问可审计:read/write/delete 留下不含明文的审计记录。
  11. 每个阶段都应独立可编译、可测试、可回滚。

迁移步骤(分阶段)

阶段依赖声明(2026-06-15 修订):本计划不再把所有阶段统一挂到同一组跨模块前置上。执行时按以下边界处理:

  1. vault-only 阶段:A 核心、B、C、F 可在当前仓库直接执行;H、I 在 C 之后执行;这些工作不等待 redaction、LoadModeSecretRef
  2. redaction 依赖:只阻塞“需要保留但必须脱敏的日志/错误输出”、跨模块脱敏测试,以及后续 SecretRef 生产化 gate;不阻塞 A 核心中“删除敏感输出”的工作。
  3. CLI LoadMode 依赖:只阻塞阶段 D 的 config secret 命令族和 validate --resolve-secrets,应与 config.md 阶段 2 共用同一框架。
  4. SecretRef 依赖:阶段 E 必须等待 config.md 阶段 5 与 mail.md 阶段 2;在此之前不得把 mail.password_ref 列入可交付范围。
  5. 阶段 B 的同步要求:最小 DB/Vault bootstrap 必须在 config.md 阶段 3 之前完成,或与其同期交付,因为 config 的 secret 命令直接依赖该能力。

阶段 A:Vault 安全止血

目标:在迁移任何配置 secret 前,消除最危险的泄露和数据丢失路径。

前置依赖(2026-06-15 修订,见《可执行性评估》):工作项 1-2"停止输出 root token / 分片 / key 文件内容"不需要脱敏工具——直接删除 println! / log::debug!、不构造含密字符串即可,现在可立即执行。脱敏工具(config 0b)只在"需保留输出的含密日志(如 DB/Redis URL)做部分脱敏"时才需要;该部分与"止血"解耦,不阻塞本阶段核心。

工作项:

  1. 删除 root token 的 stdout、stderr、tracing 输出。
  2. 删除 secret shares 和完整 key 文件内容的任何输出。
  3. VaultCore::new / VaultCore::config 改为返回 Result<Self, _>,引入专门的 VaultError(区分目录创建、key 缺失、key 不可读、格式错误、init、unseal、存储失败),并统一转换为 MegaErrorVaultError 与其 Display 不得嵌入 root token、分片或 secret 明文;不要用 MegaError::Other(format!(..)) 直接拼接敏感值。
  4. 替换初始化路径上的 expectunwrapassert
  5. fail-closed 判定以 DB 是否已初始化(rvault…inited())为准,而非 key 文件是否存在:DB 已初始化但 key 文件缺失时启动失败、绝不 delete_all();仅当 DB 未初始化且无 key 文件时才允许合法首次初始化。
  6. 显式 reset/init 作为后续运维命令设计,不能由普通启动隐式触发。
  7. Unix 下创建 vault 目录时设置 0700,创建 core_key.json 时设置 0600
  8. test_vault_reinitialize_after_file_loss 改为验证 key 缺失不会清空 vault 数据。

验收标准:

  • 启动和测试日志中不出现 root token(应有捕获日志的测试断言 token 与分片不出现)。
  • 全新(空 DB)环境仍可完成首次初始化;DB 已初始化但删除 core_key.json 后,普通启动失败且 vault 表数据不被清空。
  • 初始化失败返回可诊断错误,不 panic,且错误信息不含 token / 分片 / 明文。
  • key 文件和目录权限符合最小可读写范围。

阶段 B:拆出最小 DB/Vault bootstrap 接口边界

目标:让 vault 运维命令不依赖完整 AppContext

与 config.md 的强绑定(2026-06-14 更新):本阶段的改造结果是 config.md 阶段 3 的直接依赖。config 需要基于本阶段拆出的最小 bootstrap 能力来实现 config secret set/check 等命令。因此本阶段应在 config.md 阶段 3 之前完成,或至少同期进行,以避免 config 因等待而延期。

工作项:

  1. JupiterBackend 从依赖完整 Storage 改为依赖最小 vault storage 接口边界。
  2. 新增 VaultBackendStorage 或等价接口,只覆盖 list_keysloadsavedelete
  3. VaultStorage 成为生产 adapter。
  4. 新增 DB-only / Vault-only bootstrap 能力,只建立数据库连接和 VaultStorage
  5. 确保该 bootstrap 不初始化 Redis、对象存储、HTTP、SSH、monorepo、后台任务。

验收标准:

  • 可以只凭数据库配置构造 VaultCore
  • config validate --resolve-secrets 的底层 bootstrap 不依赖 Redis/S3。
  • vault 集成测试不需要完整服务上下文。

阶段 C:收窄 Vault interface

目标:隐藏 token、raw API path 和路径拼接规则,减少调用方误用。

工作项:

  1. 将 raw read_api / write_api / delete_api 限制在 vault 内部。
  2. 普通业务调用方只使用相对 secret name。
  3. 引入 SecretName 或等价校验,禁止以 / 开头,禁止带 secret/ 前缀。
  4. 错误信息只输出 redacted path,不输出 secret 明文。
  5. 逐步让 SSH、PGP、Nostr、PKI 调用点使用收窄后的 interface。

验收标准:

  • 普通调用方无法访问 root token。
  • secret/secret/... 这类路径重复可以在入口被拒绝。
  • 错误信息不包含明文 secret。

阶段 D:CLI LoadMode 与 vault 运维命令基础

目标:为 config secret 命令提供正确启动模型。

与 config.md 的协同设计(2026-06-14 更新):本阶段的工作项 1(在 CLI 层支持两阶段加载和 LoadMode)是与 config.md 阶段 2 的跨模块协同改造,而非独立实施。两个文档都发现了相同的需求,应作为单一设计完成。建议:

  • 先由 config + vault 团队协同设计 LoadMode 框架(定义 enum、各模式的启动路径、依赖关系等),输出为共同文档或代码(如 src/cli/load_mode.rs
  • config.md 阶段 2 和 vault.md 阶段 D 都基于这个共同框架来实现各自的子命令
  • 避免分别实施导致的设计不一致或集成冲突

工作项:

  1. 先在 CLI 层支持两阶段加载和 LoadMode(与 config.md 阶段 2 协同完成):在共同的 LoadMode 设计框架下,改造 CLI 分发逻辑以支持不同的启动模式。
  2. 新增不依赖 vault 的 mega2 config secret ref
  3. 基于最小 DB/Vault bootstrap 新增 config secret set
  4. 基于最小 DB/Vault bootstrap 新增 config secret check
  5. 新增 config validate --resolve-secrets

命令规则:

  • secret ref 只生成引用,不连接数据库,不初始化 vault。
  • secret set/check 只连接数据库和 vault,不构造完整 AppContext
  • secret set 默认只接受 --value-stdin 或隐藏输入。
  • secret set 默认不修改 TOML,只输出 vault://secret/...#field
  • 不允许通过这些命令写入数据库密码、Redis URL、当前阶段对象存储 key。

验收标准:

  • 坏配置时 config validate 能输出诊断,而不是被 CLI 预加载拦截。
  • 缺 Redis/S3 时仍可执行需要 vault 的 secret 检查,只要数据库和 vault key 可用。
  • secret 明文不进入 shell 参数、日志或错误信息。

阶段 E:SecretRef 与第一批配置凭据迁移

目标:让配置文件保存 secret 引用,而不是保存可迁移凭据明文。

首批字段:

  • mail.password -> mail.password_ref

工作项:

  1. 在配置模块中定义 SecretRef
  2. 定义 SecretResolver trait。
  3. 实现 VaultSecretResolver adapter,内部调用 VaultCoreInterface::read_secret
  4. resolver 将 vault://secret/config/prod/mail/password#value 映射为 read_secret("config/prod/mail/password"),再读取 value 字段。
  5. mail.passwordmail.password_ref 迁移期互斥:同时存在为 hard error。
  6. 消费端在 vault 就绪后通过 resolver 获取 SMTP 密码。
  7. resolver 提供缓存 TTL 和 evict / evict_all

验收标准:

  • mail.password_ref 可解析并用于 SMTP mailer。
  • secret 缺失、字段缺失、引用格式错误均返回可诊断错误。
  • 错误和日志只出现脱敏引用,不出现明文密码。
  • databaseredisobject_storage.s3.* 没有被错误迁移为 SecretRef

阶段 F:清理现有 vault 消费端 panic

目标:让 vault 内部 secret 数据损坏时可诊断、可恢复,而不是直接 crash。

优先处理:

  1. 已完成:src/server/ssh_server.rs 的 SSH server key 读取、生成、写入失败返回错误。
  2. 已完成:src/contract/vault/pgp.rs 的 PGP key 读取、解析、保存、删除返回 Result
  3. 已完成:src/contract/vault/nostr.rs 的 Nostr key 读取、生成、解析返回 Result
  4. src/contract/vault/pki.rs:PKI API 统一错误模型,避免新增 panic 风格路径。注意(2026-06-15):新 libvault 的 PKI 已按证书类型分域,调用路径随迁移更新为 pki/root/tls/generate/{internal|exported}pki/roles/tls/{name}pki/issue/tls/{role}pki/ca/tls/pem 等(旧 pki/root/generate/...pki/roles/...pki/issue/...pki/ca/pem 已不再支持,会返回 "Logical backend path not supported");错误模型清理应基于新路径,且 pki.rs 文档注释中的 crate::vault::modules::pki::* 模块引用也已随之更新。

验收标准:

  • vault secret JSON 缺字段时不会 panic。
  • secret 内容格式错误时返回包含 secret name 和字段路径的错误。
  • 错误信息不包含 secret 明文。

阶段 G:对象存储等早期依赖后置初始化(可选)

目标:只有在确实需要让对象存储凭据进入 vault 时,才重构完整初始化顺序。

2026-06-17 当前决策:本轮不迁移 object_storage.* 凭据……(历史决策,见下)

2026-06-27 落地:已实现分阶段 bootstrap,object_storage.s3.access_key_id/secret_access_key 现可配置为 vault:// SecretRef。AppContext::newsrc/context/mod.rs)只建一次 DB 连接,先做 DB-only VaultCore::from_database_connection bootstrap(不需要完整 Storage),再经 resolve_object_storage_secrets 解析对象存储凭据中的 SecretRef,最后用解析结果 build_object_storageStorage::new_with_connection(复用同一 DB 连接,不额外建连接池)。这避免了对 Storage::new 本身的高风险拆分:DB-only vault bootstrap 已足以打破循环依赖。字面量凭据原样透传,env/IAM 部署不受影响。单测见 src/context/mod.rs::tests(字面量透传 + SecretRef 解析)。

目标链路:

Config::new
  -> DbStorage::new(database 引导配置)
  -> VaultCore::new(db_storage)
  -> SecretResolver::new(vault)
  -> resolve object storage secrets
  -> Full Storage / server / task 初始化

验收标准:

  • S3/S3-compatible 凭据迁移前,服务启动链路不再在 vault 前构造对象存储。
  • 缺少对象存储 secret 时返回可诊断错误。
  • config secret set/check 对其他 secret 的操作不依赖对象存储可用。

阶段 H:Secret 访问审计

目标:让 vault secret 的访问可追溯,满足集中凭据托管的审计要求。

新架构修订(2026-06-15):libvault 的内建审计设备不可用——sys/auditsys/audit/{path} 路径虽已注册,但 handler 全部是桩实现(返回 Ok(None),见 modules/system/mod.rs:883-905)。因此本阶段只走 interface hook 路线,不依赖内建审计设备(除非愿意先补实现 libvault 的这些 handler)。审计 hook 与阶段 C 强耦合,应作为阶段 C 收窄 VaultCoreInterface 的产物之一。

工作项:

  1. 在收窄后的 VaultCoreInterface 上增加统一审计 hook(不要依赖 libvault 内建审计设备,理由见上)。
  2. 记录每次 read / write / delete 的调用方、规范化 secret name、时间、结果(成功 / 失败 / 未命中)。
  3. 审计记录对 secret 值做哈希或省略,绝不落明文;root token、分片不进入审计。
  4. 显式决定并记录审计写入失败的策略(fail-open 还是 fail-closed)。

已完成首批(2026-06-19):第 4 项已落地——VaultCore::audit_secret_accesssrc/contract/vault/integration/vault_core.rs)已补 doc-comment 显式记录fail-open策略及其理由:审计经 tracing(infallible)发出,secret 操作绝不因审计步骤被阻断/失败,这是可用性优先于不可否认性的刻意选择;该 target 仅记录 name + outcome,天然不含明文/root token/分片。

配置化首批(2026-06-23):审计现在可配置且默认开启。新增 config.vault.audit.enabledVaultAuditConfig,默认 true);组合根 AppContext::newVaultCore::with_audit_config(config.vault.audit) 注入,audit_secret_accessenabled = false 时跳过发出(运维可显式 opt-out)。config validate 已登记 vault / vault.audit 白名单,config/config.toml 附带 [vault.audit] 示例,单测 test_audit_config_is_configurable_and_defaults_enabled(默认开启 + 关闭后 secret 读写不受影响)与 vault_audit_section_is_recognized_and_validates_fields 覆盖。配置化(2026-06-27):审计 sink 现可配置——config.vault.audit.sink = "tracing"(默认,infallible)或 "file"file_path 指定的持久化 append-only JSONL,fsync 每条记录),并新增 fail_closed(默认 false)让可失败 sink 写入失败时按非否认性需要使 secret 操作失败。audit_secret_accessResult 化并由 read/write/delete 在操作成功后上抛 fail-closed 错误;单测覆盖 file sink 写入(不含 secret 值)、fail-closed 失败与 fail-open 放行。config validate 校验 sink 取值与 file 必填 file_path仍属后续:异地/远程(HTTP 等)sink —— 当前 file 为本地持久化目的地,sink 抽象可在此基础上扩展。

验收标准:

  • 每次 secret 访问产生一条不含明文的审计记录。
  • 审计记录包含足以定位调用方与 secret name 的字段。
  • 审计目的地可配置,默认开启。(已落地:config.vault.audit.enabled 默认开启、可 opt-out;持久化/异地 sink 仍为后续。)

阶段 I:root token 退役与最小权限 policy

目标:从“全程 root”过渡到按 policy 限权,符合 Vault root token 生命周期标准。

新架构修订(2026-06-15):libvault 已内建可直接接入的原语,本阶段从"自建授权体系"改为"接入并编排内建能力":

  • ACL policymodules/policysys/policy/{name}sys/policies/acl/{name},capability 含 deny/read/write/list/sudo/create/delete 等,支持 HCL。
  • 非 root tokenmodules/auth/token_store.rsauth/token/create 支持 policy 子集校验、ttl/explicit_max_ttl/period/num_uses,并有 auth/token/revoke[-orphan]renew 与后台 ExpirationManagerexpiration.rs)做租约过期。

工作项:

  1. 通过 sys/policy/{name}(或 sys/policies/acl/{name})按 secret 前缀定义 ACL policy(ssh、pgp、nostr、pki、config/* 各自最小权限)。
  2. 通过 auth/token/create 为各消费端签发带对应 policy、受限 TTL 的非 root token,替换直接使用 root token 的路径;单一 token 泄露的影响面由其 policy 子集界定。
  3. 初始化后撤销常驻 root token(auth/token/revoke/{id})。注意:当前 libvault 公开 API 没有 Vault 式 sys/generate-root 在线重建仪式(root token 仅在 init() 时产生)。因此"需要时临时重建 root 权限"必须依赖阶段 J 的恢复托管(安全保存初始 root token,或预置一个具 root policy 的恢复 token),而不能依赖在线 generate-root。
  4. core_key.json 不再长期保存可用 root token,仅保留恢复所需的分片(与阶段 J 的备份方案配合)。

验收标准:

  • 常规运行链路不持有可用 root token。
  • 任一消费端 token 泄露只影响其 policy 覆盖的 secret。
  • 仍可通过显式运维流程重建 root 权限执行管理操作。

阶段 J:轮换、rekey 与备份恢复

目标:让泄露和 key 丢失可恢复,而非只能清库重建;为 fail-closed 配齐恢复路径。

新架构修订(2026-06-15):本阶段的能力须区分"有内建原语"与"无内建原语"两类:

  • unseal 分片重新生成 ✅ 可做generate_unseal_keys()core.rs:591,用当前 KEK 重新切分分片)可封装为运维能力并保证数据不丢;但它不会轮换 KEK,旧 Shamir 分片集合仍可能组合出同一个 KEK,不能作为完整泄露恢复手段。unseal_once()core.rs:534)只会标记实际用于一次性解封的分片 deprecated,当前 10-of-5 自动解封形态下不能单独满足“旧 core_key 全部失效”的验收。详见下文“Vault 恢复运行手册”。
  • vault 加密 key(KEK)轮换 ❌ 无内建原语:KEK 在 init() 后不可变,crate 未提供重加密屏障 / sys/rotate 等价能力。真正的 KEK 轮换需自建(密封 → 以新 KEK 重加密全部数据 → 重切分分片,本质等价于一次受控迁移,Core::migrate() 仅能搬运后端数据、不等于轮换),应另立专项;在该专项落地前本阶段不承诺 KEK 轮换,验收标准也不应包含它。

工作项:

  1. ✅ 提供 unseal 分片 rekey 的运维命令(基于 generate_unseal_keys() / unseal_once())——已落地 config vault rekey --force [--key-path](见下文“重新生成 unseal 分片”与“已完成(2026-06-27)”)。vault 加密 key(KEK)轮换因无内建原语,单列为后续专项,不在本阶段交付(见上)。
  2. ✅ 定义密钥材料(分片 / 恢复凭据)的安全托管与备份位置(外部密钥管理系统 / 离线托管),写入下文“Vault 恢复运行手册”。可执行入口已落地:config vault backup <DESTINATION> [--key-path <PATH>]core_key.json 复制到目标位置并生成 .meta.json 元数据;备份文件在 Unix 下权限设为 0600
  3. ✅ 定义“DB 数据在、key 丢失”的恢复流程,以及疑似 core_key.json 泄露后的 rekey 流程。可执行入口已落地:config vault restore <SOURCE> --force [--key-path <PATH>] 先将备份复制到临时文件,调用 VaultCore::from_database_config 验证该 key 能解封当前数据库,再原子替换 core_key.json;若验证失败则保留原 key 文件不变。疑似泄露后应先用 config vault rekey 重写分片,再视风险决定是否重建 vault。
  4. 为可迁移 secret(首批 mail.password)提供轮换支持。 5.(可选,长期)评估外部 KMS / transit auto-unseal,替代本地落盘自动解封,缓解磁盘读取威胁。

已完成首批(2026-06-19):第 4 项已落地——新增 mega2 config secret rotate <field> --vault-path ... --field ... --value-stdinsrc/commands/config.rs),经最小 DB/Vault bootstrap 覆写可迁移 secret(首批 mail.password),复用 set 的 namespace 校验与脱敏,并显式打印重启要求:运行中的 service 在 AppContext::new 一次性解析 mail.password_ref,因此需重启才能 re-resolve;config validate --resolve-secrets 与后续新 resolve 立即使用轮换值。这满足"明确其重启要求"的验收口径。运行期热生效(动态 mailer 重建)仍属 mail 阶段 4。

已完成(2026-06-27):第 1 项已落地——新增 mega2 config secret rotate 之外的 vault 运维命令 mega2 --config <path> config vault rekey --force [--key-path <PATH>]src/commands/config.rsexec_vault_rekey + vault_rekey_cli)。该命令经最小 DB/Vault bootstrap(LoadMode::VaultBootstrap)打开当前 vault 并调用既有原语 VaultCore::rekey_unseal_shares() 重写 core_key.json 的 Shamir 分片,保留数据;--force 必填、--key-path 可覆盖 key 位置;成功后显式打印“旧分片仍可解封本 vault、彻底失效需 KEK 轮换”的限制。新增 CLI 解析/load-mode 单测(config_vault_rekey_uses_vault_bootstrap_load_modeconfig_vault_rekey_requires_forceconfig_vault_rekey_accepts_key_path)。这把此前“仅库内方法 + 单测、无运维命令”补齐为“有运维命令”,满足第 1 项的运维命令验收口径。

已完成(2026-06-28):第 2–3 项已落地——新增 config vault backup <DESTINATION> [--key-path <PATH>]config vault restore <SOURCE> --force [--key-path <PATH>] 运维命令(src/commands/config.rsexec_vault_backup / exec_vault_restore + vault_backup_cli / vault_restore_cli;核心实现为 VaultCore::backup_key / VaultCore::restore_key)。两个命令均走 LoadMode::VaultBootstrap--key-path 可覆盖默认 core_key.json 位置;restore 必须 --force 确认。backup 将 key 文件复制到目标位置并写入 .meta.json(记录来源路径与备份时间),Unix 权限 0600;restore 先把备份复制到 core_key.json.restore-tmp,用 VaultCore::from_database_config 验证能解封当前数据库,再原子替换原 key 文件,验证失败时保留原 key 文件并返回错误。新增 CLI 解析/load-mode 单测(config_vault_backup_uses_vault_bootstrap_load_modeconfig_vault_backup_accepts_key_pathconfig_vault_restore_uses_vault_bootstrap_load_modeconfig_vault_restore_requires_force)与核心功能单测(test_backup_key_creates_key_and_meta_filetest_restore_key_verifies_and_replaces_key_filetest_restore_key_rejects_backup_that_does_not_unlock_vault)。

验收标准:

  • ✅ 分片重新生成后新的 core_key.json 可解封且数据不丢——已由 config vault rekey 运维命令封装 VaultCore::rekey_unseal_shares() 提供(2026-06-27);旧分片彻底失效需等待 KEK 轮换或外部 KMS / transit auto-unseal 专项。
  • ✅ 文档化的恢复运行手册可在 key 丢失(数据在)场景下恢复访问或安全重置——已由 config vault backup/restore 运维命令与运行手册配套落地(2026-06-28)。
  • secret 轮换不需要重启全部依赖该 secret 的服务,或明确其重启要求。✅ 已通过 config secret rotate + 显式重启提示满足"明确重启要求"分支(2026-06-19)。

Vault 恢复运行手册

本运行手册记录阶段 A/J 加固后应遵循的 Vault 运维行为,重点覆盖 core_key.json 备份、恢复、显式重置、unseal 分片重新生成,以及 root token 恢复材料的边界。

适用范围

  • core_key.json 保存嵌入式 RustyVault 实例的本地自动解封密钥材料。
  • 数据库 vault 表保存加密后的 Vault 数据。
  • 当数据库已经初始化但 core_key.json 缺失时,服务启动必须故障关闭(fail-closed);普通启动不得删除 vault 表数据。

正常备份

  1. 使用运维命令将 core_key.json 备份到安全位置:

    mega2 --config <path> config vault backup /secure/backup/path/vault-key.bak

    该命令会复制 mega_base()/vault/core_key.json 到目标位置,并在同目录生成 .meta.json 元数据文件,记录来源路径和备份时间。若目标路径是目录,命令会自动生成带时间戳的文件名。备份文件在 Unix 下会被强制设为 0600 权限。

  2. 将备份副本加密保存到应用主机之外的外部密钥管理系统、离线加密介质,或等效的受限凭据系统中。

  3. 除非快照本身已加密并有访问控制,否则 core_key.json 及其备份必须排除在容器镜像、日志采集、源码控制、支持包和普通文件系统快照之外。

  4. Unix 环境下,保持 vault 目录权限为 0700core_key.json 与备份文件权限为 0600

DB 数据存在但 key 文件缺失时的恢复

  1. 停止 mega2。

  2. 使用恢复命令将验证过的备份 key 原子替换到 core_key.json

    mega2 --config <path> config vault restore /secure/backup/path/vault-key.bak --force

    该命令会先把备份复制到临时文件,调用 VaultCore::from_database_config 验证该 key 能解封当前数据库,再替换 core_key.json;如果验证失败(备份与当前数据库不匹配或备份损坏),原 key 文件保持不变,命令返回错误。

  3. 手动设置权限(restore 已在 Unix 下将新 key 文件设为 0600,仍需确认目录为 0700):

    chmod 700 "$(dirname "$CORE_KEY_PATH")"
    chmod 600 "$CORE_KEY_PATH"
  4. 启动 mega2。

  5. 确认依赖 Vault 的消费者可以正常读取 secret。

如果没有匹配的密钥材料,按当前嵌入式 RustyVault 设计,已加密的 Vault 数据无法恢复。不要期望服务启动后自动重新初始化;它必须故障关闭(fail-closed)。

不需要恢复旧数据时的显式重置

仅当丢失所有 Vault secret 可以接受时,才允许使用本流程。

  1. 停止 mega2。
  2. 备份数据库和任何现存的 core_key.json
  3. 在受控维护流程中删除 vault 表数据,或重建数据库。
  4. 删除旧的 core_key.json
  5. 启动 mega2,使其针对未初始化的 vault store 执行首次初始化。
  6. 重新创建必要的 secret。

普通服务启动绝不能隐式执行这个重置。

重新生成 unseal 分片

运维命令:mega2 --config <path> config vault rekey --force [--key-path <PATH>]src/commands/config.rsexec_vault_rekey)。该命令经最小 DB/Vault bootstrap 打开当前 vault,调用 VaultCore::rekey_unseal_shares()(底层 RustyVault 的 generate_unseal_keys()),用当前 KEK 的新 Shamir 分片集合重写 core_key.json,保留 Vault 数据。它是 config vault reset 之外第二个显式 vault 运维命令,沿用 LoadMode::VaultBootstrap,必须通过 --force 显式确认,--key-path 可覆盖默认 core_key.json 位置。

当前限制:RustyVault 只是重新切分同一个 KEK。之前导出的 Shamir 分片集合仍可能恢复该 KEK,因此如果旧分片已经泄露,这不是完整的泄露恢复手段。命令在成功后会显式打印这一限制,提示旧分片集合仍可解封本 vault。要彻底使旧密钥材料失效,需要 KEK 轮换,或引入外部 KMS / transit auto-unseal 设计;这超出当前 vendored RustyVault 原语能力。

Root token 处理

Rust 应用接口不再向普通调用方暴露 root token,secret 操作通过收窄后的 secret interface 和审计 hook 执行。当前本地自动解封文件仍保存兼容与恢复所需的 root 恢复材料。要安全移除这部分材料,必须另行设计 root recovery token 或外部凭据托管机制;如果没有恢复路径就直接移除,未来维护可能变得不可执行。

与其他文档的协调关系(2026-06-15 更新)

vault 阶段 主要工作 对 config 的依赖 对 mail 的依赖 对 notification 的依赖
A (P0) 安全止血 核心止血无依赖;仅“保留输出的脱敏”依赖 config 0b 支持后续日志脱敏 支持后续日志脱敏
B (P1) 最小 bootstrap 拆分 → config 3 依赖此
C (P1) 收窄 interface + 审计 hook 入口 支持后续类型安全 支持后续类型安全
D (P1) CLI LoadMode ← 与 config 2 协同 支持后续运维 支持后续运维
E (P2) SecretRef 迁移 与 config 5 协同 mail 作为第一消费者 后续支持
F/H/I/J (P1-P4) 消费端错误模型、审计、最小权限、rekey/恢复 F/H/I/J 的 vault-only 子集无 config 前置;secret 轮换与 SecretRef 部分依赖 E
G (P4) 对象存储后置初始化 ← 依赖 config 7 或同等初始化顺序重排

关键同步点:

  1. 日志脱敏工具(来自 config 0b)→ vault 中需要保留的敏感上下文输出:不阻塞 A 核心止血;阻塞跨模块统一脱敏、错误诊断脱敏和 SecretRef 生产化 gate。
  2. vault B 完成 → config 3 依赖:config 必须等待 vault 的最小 bootstrap 拆分,或与其在同一改造中交付。
  3. CLI LoadMode 框架(config 2 与 vault D 协同):两个文档需共同设计而非分别实施。
  4. config 5 + mail 2 → vault ESecretRefmail.password_ref 必须等 resolver 与 mail 侧消费形态就绪后再落地。

前置依赖矩阵

本文档各阶段与其他文档/能力的依赖关系如下(详见上文「跨模块协同前置」):

本文档的工作 对其他文档的依赖 类型 关键同步点
A 安全止血(Result 化、fail-closed、权限、敏感不输出) 无(vault-only,可立即推进) 前置 删除敏感输出不依赖 redaction 模块
A 脱敏(需保留输出时) config.md redaction(src/config/redaction.rs 协同 redaction 已落地,供需要保留的 URL 日志使用
B 最小 bootstrap 无(vault-only) 前置 config 阶段 3 的最小 bootstrap 直接复用本能力
C 接口收窄 无(vault-only) 前置 H 审计 hook 接在收窄后的 interface 上
D/E CLI 与 SecretRef config.md LoadMode、SecretRef/resolver 后置 与 config 阶段 2/5 共用 LoadMode,mail 作为首个消费者
G 对象存储凭据 SecretRef config.md 对象存储后置初始化 协同 DB-only vault bootstrap 后解析对象存储 SecretRef(启动路径,validate/CLI 未对齐)
H 审计 收窄后的 VaultCoreInterface(C) 后置 走 interface hook,不依赖 libvault 桩审计设备
I root token 退役 libvault ACL policy / token(内建) 前置 编排内建能力,迁移更多凭据前完成
J 轮换/rekey libvault unseal rekey(内建) 部分 分片 rekey 可用;KEK 轮换无内建原语,须另立专项

推荐优先级

优先级 工作 原因 前置
P0 删除 root token / 分片 / key 文件内容输出,初始化返回 Result / VaultError(不泄敏) 迁移任何 secret 前的安全止血;直接删除敏感输出即可
P0 以”DB 是否已初始化”为准的 fail-closed,替换清库重建的行为和测试 旧行为会灾难性丢数据;纯文件判定又会误伤全新部署
P0 core_key.json 与 vault 目录权限收紧 与 fail-closed 同属 key material 加固,改动局部且可测
P0 并行 建立日志脱敏工具(可作为独立前置) 阻塞“保留但脱敏输出”、跨模块错误诊断与后续 SecretRef 生产化,不阻塞 A 核心 无(独立)
P1 拆出最小 DB/Vault bootstrap 接口边界 config secret set/check 与 config 3 的必要条件;应在 config 3 之前或同期完成 阶段 A 核心
P1 收窄 vault interface,隐藏 token 和 raw path,并预留审计 hook 降低误用和泄露风险;阶段 H 依赖统一入口 阶段 A/B
P1 Secret 访问审计(阶段 H) 集中凭据托管的审计基线;只能走 interface hook 阶段 A/B/C
P1 清理 SSH/PGP/Nostr panic 提升 vault 数据损坏时的可恢复性,减少 SecretRef 迁移前的 crash 面 阶段 A/B/C
P1 root token 退役与最小权限 policy(阶段 I) 避免”全程 root”,符合 root token 生命周期 阶段 A/B/C/H
P1 key 丢失 / 泄露的备份恢复运行手册(阶段 J 第 2–3 项) fail-closed 必须配套恢复路径 阶段 A
P1 协同 设计 CLI LoadMode 框架(与 config 协同) 阶段 D 与 config 2 的必要条件 无(协同)
P2 config secret ref/set/check 形成标准运维入口 阶段 B、D + config 3/4
P2 SecretRef + resolver + mail.password_ref 第一批可迁移凭据 阶段 A/B/C/H/I + config 5 + mail 2
P3 unseal 分片 rekey 与 secret 轮换(阶段 J 第 1、4 项)✅ 已交付 config vault rekey + config secret rotate 泄露后可恢复,不必清库重建;分片 rekey 有内建原语 阶段 A/B/E
P4 KEK 轮换专项(无内建原语,需自建重加密流程) crate 未提供 sys/rotate 等价能力,须单独立项 阶段 A/B/J
P4 对象存储后置初始化 只有 S3 凭据要进 vault 时才需要 阶段 A/B + config 7
P4 外部 KMS / transit auto-unseal 缓解磁盘读取威胁,需部署侧支持 阶段 A/B/J

架构 deepening 机会

将 VaultCore deepening 为 secret store module

涉及文件:

  • src/contract/vault/integration/vault_core.rs
  • src/contract/vault/pgp.rs
  • src/contract/vault/nostr.rs
  • src/server/ssh_server.rs

问题:调用方需要知道 token、path、JSON shape、字段名和错误行为,interface 过宽且接近 implementation。

方案:让 VaultCore 对外只提供规范化 secret 操作,业务模块通过 typed helper 读取自己的 secret。path 映射、redaction、错误分类集中在 vault module 内部。

收益:调用方 interface 更小,测试 surface 更清晰,secret shape 变更的 locality 更好。

将 JupiterBackend 改为最小 storage 接口边界

涉及文件:

  • src/contract/vault/integration/jupiter_backend.rs
  • src/jupiter/storage/vault_storage.rs
  • src/context/mod.rs

问题:vault 只需要 vault table,却依赖完整 Storage 生命周期。

方案:引入最小 VaultBackendStorage 接口边界,让 VaultStorage 成为 adapter。JupiterBackend 不再知道完整 Storage

收益:config secret 命令不被 Redis、S3、monorepo 初始化阻塞;vault 测试更容易隔离。

将 SecretRef resolver 放在配置 module

涉及文件:

  • 未来 src/config/secret.rs
  • src/contract/vault/integration/vault_core.rs

问题:VaultCore 管 secret 存储,SecretRef 管配置引用,两者语义不同。

方案:在 config module 定义 SecretRefSecretResolver,由 VaultSecretResolver 作为 adapter 连接 vault。

收益:Config::new 继续只做同步配置解析,secret 解析作为 vault 就绪后的独立异步阶段,符合启动依赖顺序。

vault 运维命令使用 LoadMode

涉及文件:

  • src/cli.rs
  • src/commands/mod.rs
  • 未来 src/commands/config.rs

问题:当前 CLI 在分发任何子命令前会加载完整配置,完整 AppContext 又会初始化过多依赖。

方案:先实现两阶段 CLI 和 LoadMode,再让 config secret 选择 VaultBootstrap,让 service 选择 FullAppContext

收益:无配置、坏配置、缺 Redis/S3 时仍能执行配置诊断和 secret 运维。

评估 PKI 原生 ssh/pgp 能力替代 KV 裸私钥存储(新架构机会,2026-06-15)

涉及文件:

  • src/contract/vault/pgp.rs
  • src/contract/vault/nostr.rs
  • src/server/ssh_server.rs
  • src/vault/modules/pki

问题:当前 SSH host key、PGP、Nostr 私钥都以 KV secret(read_secret / write_secret)形式存储并各自手工管理 JSON shape。新 libvault 的 PKI 已原生支持 tls / ssh / pgp 三类证书的签发、存储与吊销(issue/ssh/*roles/ssh/*revoke/(tls|ssh|pgp)certs/(tls|ssh|pgp)/* 等),具备角色、序列号、CRL 等生命周期能力。

方案:评估把 SSH / PGP 由"KV 存裸私钥"迁移为"PKI 原生签发与托管",统一证书生命周期(吊销、CRL、序列号、审计)。Nostr 若无证书语义则维持 KV。此为可选架构机会,非阶段 A–J 的硬性前置;若采纳应在阶段 F / I 之后单独立项,并评估对现有已存储 key 的兼容迁移路径。

收益:证书的吊销、CRL 与审计由 vault 统一管理,减少各 vault/*.rs 自管 JSON shape 的分散逻辑与 panic 面。

下一步建议

重要:本次任务仅完成文档分析与修订,未执行任何代码改动或验证命令。 所有实现工作须由后续独立变更单独负责,并必须满足 AGENTS.md 全部要求。

第一批 PR 应只做 P0,范围严格控制在 vault 安全止血(对应 A1+A2+A3 切片):

  1. VaultCore::new / VaultCore::config 改为返回 Result,引入不泄敏的 VaultError
  2. 删除 root token、分片、key 文件内容的所有输出(stdout / stderr / tracing)。
  3. fail-closed 以 rvault…inited() 为准:DB 已初始化但 key 缺失即失败、不清库;空 DB 无 key 仍可合法首次初始化。
  4. key 文件和目录创建时设置权限(目录 0700core_key.json 0600)。
  5. test_vault_reinitialize_after_file_loss 改为:DB 已初始化时缺 key 不清空数据,并新增空 DB 首次初始化用例。
  6. 更新调用方(至少 AppContext::new 返回错误传播、ssh_server.rs 读取/生成失败处理),让启动失败返回可诊断错误。注意:此项会引入少量 context/commands 侧适配,属于 A2 允许的最小波及范围。

首批 PR 执行边界(2026-06-16 分析更新):

  • 允许改动:src/contract/vault/integration/vault_core.rs 及其 tests、src/contract/vault/integration/jupiter_backend.rs(若 B 同期小步)、src/context/mod.rs(最小错误传递)、src/server/ssh_server.rs(最小错误返回)、vault 其他消费端中仅初始化/读取路径的 panic 清理(F 可后续批次)。
  • 允许新增:集中定义在 src/common/errors/VaultError / VaultResult、Unix 权限辅助(cfg 守卫)、只覆盖初始化语义与 fail-closed 的测试用例。
  • 绝对禁止(本阶段):config secret 命令族、LoadMode、SecretRef / resolver 实现、mail password 迁移、对象存储初始化顺序重排、KEK 轮换、libvault 源码修改、redaction 模块。
  • 评审重点:启动日志和错误字符串不得含 root token / 分片 / 明文 secret;DB 已初始化但 key 缺失时绝不调用 delete_all();空 DB 首启仍可成功初始化并写 key;所有新增失败路径返回错误而不是 panic;权限代码在非 Unix 平台不放宽行为。
  • 强制前置验证(AGENTS.md):实现 PR 在任何 push / review 前必须本地通过:
    • cargo +nightly fmt --all --check(必须 clean,无 diff)
    • cargo clippy --all-targets --all-features -- -D warnings(必须 0 warning、0 error;不得用 blanket allow 掩盖)
    • source .env.test && cargo test --all(必须全部通过)。若当前环境缺少 .env.test,实现前必须向环境维护者索取;严禁省略此步骤或用无 DB 测试冒充。
    • 额外:cargo buildcargo build --tests 应 0 error 0 warning(作为快速烟雾)。
  • 变更哲学:最小化、跟随现有模式(复用 jupiter tests 辅助、MegaError、tracing 日志而非 println、snake_case 文件等)。新增类型作用域尽量小。

Libra 工作区检查:

  • 开工前运行 libra status,确认已有改动范围;不要回滚与本阶段无关的文件。
  • 核对文档或代码差异时使用 libra diff -- <path>,例如 libra diff -- docs/refactoring/vault.md
  • 提交或评审时只纳入本阶段允许范围内的文件;若工作区已有不相关改动,应保持原样并在 PR / 交付说明中标明未触碰。

这一步完成前,不建议开始 SecretRefmail.password_ref 或对象存储凭据迁移。备份恢复运行手册(P1)应与 fail-closed 同期或紧随其后落地——否则 fail-closed 会把“key 丢失”从“自动重建”变成“无法恢复”。审计(阶段 H)与 root token 退役(阶段 I)应在迁移更多生产凭据前完成,以满足 Vault 安全标准。

与本次任务的边界说明:2026-06-16 的本次变更仅修改了本规划文档(插入可行性分析小节、更新日期/边界表述、强化 AGENTS 门禁与实施提示),未改动任何 src/ 代码、Cargo.toml、测试或配置。文档修订本身不触发构建/测试门禁,但为未来真实落地提供了经核查的执行依据。后续任何实际编码任务必须独立开启、独立评审、独立通过三大门禁。

风险与约束

  • 风险:自动解封材料落盘。 unseal 分片存于本地 core_key.json,磁盘读取攻击者可解封。
    • 影响:vault 静态机密性取决于文件系统访问控制,而非密码学保险箱。
    • 缓解措施:Unix 0700/0600 权限、部署侧 KMS/受控挂载、备份恢复与恢复演练。
  • 风险:fail-closed 使 key 丢失不可自动恢复。 key 缺失时不再清库重建。
    • 影响:缺少备份时 key 丢失等于数据不可解。
    • 缓解措施:备份恢复运行手册必须与 fail-closed 同期落地。
  • 风险:日志脱敏守卫存在已知留白。 回归测试只捕获本任务线程的 tracing 事件。
    • 影响:stdout/log:: facade/vault 后台 OS 线程上的潜在泄露不被该单测覆盖。
    • 缓解措施:评审时人工核对新增日志点;如需彻底覆盖再引入全局 subscriber/fd 捕获。
  • 约束:KEK 轮换无 libvault 内建原语。
    • 理由:init() 后 KEK 不可变,无 sys/rotate 等价能力。
    • 影响:彻底使旧 unseal 分片失效需另立 KEK 轮换专项;本计划只承诺分片 rekey。
  • 约束:审计 fail-open 为默认。
    • 理由:审计经 infallible 的 tracing 不阻断 secret 操作(可用性优先)。
    • 影响:需要 non-repudiation 的部署应显式设 config.vault.audit.fail_closed = true(仅对 file sink 生效)。

改进方案多维评估小结

维度 评估结论
合理性 高(9/10)。准确区分 vault-only 止血队列(A/B/C/F)与跨 config 队列(D/E/G),避免把可立即落地的安全止血误判为被 redaction/LoadMode 阻塞。当前不足:早期文档口径需持续收敛。
可行性 中高(7.5/10)。P0/P1 加固已落地,剩余主要为工程化与运维面。改进方向:远程审计 sink 与 KEK 轮换专项评估。
完整性 较高(8/10)。覆盖初始化语义、fail-closed、权限、审计、最小 bootstrap、SecretRef、轮换。不足:备份恢复演练与跨平台权限等价机制仍需补。
安全性 强(8.5/10)。root token 退役、限权 token、ACL 隔离、敏感不输出、审计可 fail_closed 均已落地。残余风险限定为静态解封材料与部署侧托管。
功能正确性 良好(8.5/10)SecretName 校验、fail-closed 判定以 inited() 为准、限权 token ACL 隔离均有测试。当前不足:跨平台权限语义待补。
可靠性与容错性 良好(8/10)。错误传播替代 panic、审计 fail-open/closed 可选、reset/rekey 显式运维命令。改进方向:多实例并发与恢复演练。
兼容性 良好(8/10)。vendored libvault 能力面已核查,PKI 域路径已对齐;KEK 轮换缺内建原语已显式声明。
可维护性与可扩展性 良好(8.5/10)VaultCore/VaultCoreInterface 边界清晰,审计/SecretRef/命令可独立扩展。

小结

mega2 的 vault 模块已从“可用但有安全缺口”的 KV store 加固为 fail-closed、限权、可审计、可最小 bootstrap 的 secret 后端:VaultCore Result 化、key 缺失不清库、root token 退役、限权 token ACL 隔离、敏感材料不进 tracing 日志、审计可配置(tracing/file、可 fail_closed、含 caller)、SecretRef resolver 支撑 mail/notification/对象存储凭据 post-vault 解析,并提供 config vault reset/rekeyconfig secret set/check/rotate 运维命令。唯一明确不承诺的是 KEK 轮换(无 libvault 内建原语,须另立专项)。

预期收益

  • 安全止血闭环:root token/分片/secret 明文不再主动写入 tracing 日志(初始化任务线程有回归测试守卫,捕获边界留白见「风险与约束」),DB inited 但 key 缺失 fail-closed 不再静默清库。
  • 最小权限与隔离:常规 secret 访问用限权 token,config/generic ACL 隔离有矩阵测试,降低单点凭据失守的爆炸半径。
  • 可审计性:read/write/delete 记录 vault_audit(含 caller),sink 可选 tracing/file,可按需 fail_closed,满足 non-repudiation 取舍。
  • 最小 bootstrap 运维:vault 运维命令不依赖完整 AppContext,可在裸机/初始化阶段执行。
  • SecretRef 基础设施就位:mail/notification/对象存储凭据可走 vault SecretRef,在 vault 就绪后解析,配置与秘密分离。
  • 可恢复的轮换路径:unseal 分片 rekey 与可迁移 secret rotate 有显式命令;KEK 轮换边界已明确,避免过度承诺。