本文档记录 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 status、libra diff -- <path>、libra add等 Libra 命令检查工作区状态与差异;不要把git status/git diff失败误判为“不是仓库”。本文中“Git 协议 / Git 托管 / 不进入 git”描述的是 mega2 的业务域和兼容目标,不代表当前开发工作区必须由 Git 管理。
本文档中的代码引用已对照当前
src/(含src/contract/vault/integration/、src/vaultvendored 源码、src/context/mod.rs、src/commands)重新核对。以下校准说明(依赖迁移、落地可行性、落地状态更新)按时间顺序记录与早期草案不一致的事实,后续执行以本节、下方「当前实现状态速览表」和「硬约束与不可违反的原则」为准,不要按旧阶段重复实现。
依赖迁移修订(2026-06-15,2026-06-17 路径与模块更新):
libvault-core(crates.io0.1.0)已替换为仓库内 vendored 的 RustyVault 源码模块(src/vault/目录,作为 mega2 顶层 module 编译,不再作为独立 path dependency)。新依赖的能力面与旧版不同,本文相关阶段已据此核查与修订,主要影响:
- 阶段 I(root token 退役 / 最小权限):libvault 已原生提供 ACL policy(
modules/policy,sys/policy/{name})与非 root token(modules/auth/token_store.rs,auth/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/internal、pki/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/vaultvendored 源码、其他 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/audithandler 存在但按修订说明为桩(handler 返回 Ok(None));PKI 域路径已在 pki.rs 中部分对齐。阶段 H 走 interface hook、I 编排内建、J 分片 rekey 的判断均成立。- 跨模块阻塞判断准确:
src/中不存在 redaction 模块、LoadMode、SecretRef/resolver、config 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 子集)都必须在提交前通过:
cargo +nightly fmt --all --check(无 diff)cargo clippy --all-targets --all-features -- -D warnings(0 warning/0 error)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_CALLER与with_audit_caller;audit_secret_access在vault_audit事件中记录caller(未注入时为unknown);config secret set/check/rotate、config 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):用线程局部tracingsubscriber 捕获 VaultCore 集成在 init → write_secret → read_secret → reset 全流程于初始化任务线程上发出的tracing事件,断言写入的 secret 明文、已持久化的 unseal 分片(compact JSON / pretty JSON / Debug 三种形态,pretty 对应persist_core_key的to_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 已引入
LoadMode,config secret ref/set/check与config validate --resolve-secrets已落地;secret set/check使用最小 DB/Vault bootstrap,不构造 Redis、对象存储、服务或完整AppContext。SecretRef、SecretResolver、VaultSecretResolver已在配置模块落地,mail.password_ref可在 vault 就绪后解析,且与明文mail.password互斥。I:常规 secret 读写不再使用 root token。初始化时用 root token 安装 mega2 运行时 ACL policy、签发 ssh/pgp/nostr/pki/config/generic 限权 token,随后写回不含
root_token的core_key.json并撤销 root token。为支持重启后限权 token 的 ACL 校验,vendoredlibvault的 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-onlyVaultCorebootstrap(from_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 validate与config secret set/check现已接受合法 namespace 的 object_storage SecretRef,config validate --resolve-secrets也会解析它们。
本节由
docs/plan/plan-20260820.mdTask VLT-00 冻结,是「vendoredsrc/vault/改用 crates.iolibvault依赖」这一反向迁移的事实源。上文「依赖迁移修订(2026-06-15)」记录的是正向迁移(libvault-core0.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 |
| 错误类型 | 本地 RvError(src/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,368 与 src/contract/vault/pki.rs:80,114(doc-link);RvError 另经 src/contract/vault/integration/jupiter_backend.rs、src/common/errors/ |
全部改写为 libvault::*,rg 'crate::vault' src bin 零命中 |
| 非切换面(误记纠正) | src/context/、src/server/ssh_server.rs、src/config/、src/commands/ 只消费 contract::vault::integration;src/jupiter/storage/vault_storage.rs、src/callisto/ 是 SeaORM 实体 |
不进引用切换写集;只有只读入口签名面随 VLT-02/04 调整 |
| 只读原语 | Core.readonly(src/vault/core.rs:110)、Core::new_readonly(:182)、MountsRouter::load_readonly、AuthModule::load_auth_readonly、ExpirationManager::restore_readonly / is_lease_checker_started、RustyVault::new_readonly——上游零命中 |
一律不搬迁;只读语义在 src/contract/vault/ 重建(VLT-04) |
| 只读保险 | ReadonlyBackend(src/vault/storage/readonly.rs) |
移动到 src/contract/vault/integration/readonly_backend.rs 并重挂错误标识 |
| 测试 | src/vault/fix04_list_contract.rs、src/vault/un31_readonly.rs(挂载于 src/vault/mod.rs:49,59) |
迁至 src/contract/vault/integration/{fix04_list_contract,un31_readonly}.rs |
| 只读消费点 | VaultCore::open_readonly(src/contract/vault/integration/vault_core.rs:361)→ src/context/mod.rs:436;断言在 src/context/un43_readonly_assembly.rs:300-302、bin/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 升级。
- 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):只读可行性门禁——上游
libvault0.3.0 的post_unseal(../libvault-rs/src/core.rs:608-631)是私有方法且无 readonly 分支,无条件执行mounts_router.load_or_default与module_manager.init(后者经../libvault-rs/src/modules/auth/mod.rs:411-412启动过期租约 worker)。因此删除 vendored 必须排在只读可行性结论之后,由 Task 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 的逐项否证/采纳表。
来源: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_policy;token_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_readonly;is_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 保持绿。
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 build0 错误 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.0、enum-map 2.7.3、pem 3.0.6、ipnetwork 0.17.0、derive_more 0.99.20、hcl-rs 0.18.7、radix_trie 0.2.1、stretto 0.8.4、bcrypt 0.17.1、base64 0.22.1。本仓在此之前已有rand/rand08双版本先例,故不做单版本收敛(DEFER-VLT-03)。 - 编译面:
storage_pg与crypto_adaptor_openssl两个特性显式开启;crypto_adaptor_openssl是上游default,此处显式写出以免将来default-features变化时静默丢失。
结论: 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-631(post_unseal 私有、无条件 load_or_default + module_manager.init) |
否证:只能写时拒绝,不满足写前具名 fail-closed;worker 仍启动 |
| 2 | 替换 Auth / Policy module(自定义 Module 实现) |
../libvault-rs/src/core.rs:279,356,369、modules/system/mod.rs:724,781,819,836,971,1072、modules/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 的全部字段为 pub(barrier / mounts_router / module_manager / state / mounts_monitor / mount_entry_hmac_level …) |
../libvault-rs/src/core.rs:89-102 |
RustyVault.core 为 pub 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_PREFIX 为 pub const |
../libvault-rs/src/storage/barrier_view.rs:51,58;mount.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 / .hmac 为 pub,可在集成层重建「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 |
只有 AuthModule 与 PolicyModule 覆写 Module::init;SystemModule / PkiModule / KvModule / CertModule 只覆写 setup(纯注册,无写),其 init 取 trait 默认 Ok(()) |
../libvault-rs/src/modules/mod.rs:78-80;modules/{pki,kv,credential/cert}/mod.rs 的 impl Module |
AuthModule 的 token_store / expiration / mounts_router / barrier 为 pub,add_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,186;auth/token_store.rs:150;policy/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 |
RustyVault::new(backend, Some(&cfg)),其中cfg.mounts_monitor_interval被强制置 0——「不创建 mounts monitor」由只读模式决定,与调用方配置无关(AC5 前半)。core.barrier.inited()/sealed()/seal_config()预检 →ShamirSecret::combine(阈值为 1 时直接取分片)→core.barrier.unseal(kek)。- 装配
CoreState:CoreState::default()后写hmac_key = barrier.derive_hmac_key()、system_view = BarrierView::new(barrier, SYSTEM_BARRIER_PREFIX)、sealed = false。私有的kek/unseal_key_shares不需要——它们只服务generate_unseal_keys(),那是写路径。 - 只读 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)。 - 只读版 auth init 的顺序必须与上游一致:先
auth.token_store.store(..),后才轮到 policy 的core.add_auth_handler(..)——后者经AuthModule::set_auth_handlers对token_store无条件unwrap()(modules/auth/mod.rs:78-84),顺序颠倒会 panic。
| AC | 重建手段 | 观测面(公共 API) | 原型断言 |
|---|---|---|---|
| AC1 mount | MountTable::load + 自建 needs_mount_update 扫描,两种情形均具名 fail-closed |
具名错误 + storage 快照 | un31_a_missing_mount_table_fails_closed、un31_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_replanted、un31_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_worker、un31_an_older_format_lease_fails_closed、un31_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)。
| # | 脆弱点 | 缓解 |
|---|---|---|
| 1 | 只读 post-unseal 是上游私有 post_unseal 的影子实现;上游若在其中新增步骤,只读路径不会自动跟进 |
UN-31 十二用例 + 逐字节快照断言;升级 libvault 时按本节复核 |
| 2 | token salt 路径 "token/" + "salt" 是上游私有常量 TOKEN_SUB_PATH / TOKEN_SALT_LOCATION(auth/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_handlers 对 token_store 无条件 unwrap(),只读 init 的顺序不能变 |
顺序写入代码注释 + un31_a_readonly_open_reads_what_the_writable_one_stored 会在顺序错时 panic |
计划「事实基线」把 vendored↔上游差异记为「UN-31 只读 + RvError 本地化 + 依赖版本面」。逐文件核对(剔除纯 import 改写与 doc 后共 24 个文件有差异)另发现两处功能性分歧,不在只读轴内,必须随 VLT-02/04 处置:
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 的强制回归观测点,不是可选项。modules/auth/token_store.rs:457:上游为log::debug!("check token: {token}"),会把明文 client token 写进 debug 日志;vendored 已脱敏为log::debug!("check token")。本仓未安装任何logfacade logger、也未接tracing-log桥(rg 'tracing_log|LogTracer|log::set_logger' src bin零命中),因此该语句在 mega2 内不产生输出,属残留风险而非现存泄漏;一旦将来接入log桥必须重新评估(登记为DEFER-VLT-04)。
生产落点 2 个文件,与计划的 scope=S 一致:
src/contract/vault/integration/vault_core.rs:open_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 中间态桩)。
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_readonly、Core::new_readonly、MountsRouter::load_readonly、AuthModule::load_auth_readonly、ExpirationManager::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.rs 的 load_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.rs 的 un43_a_needed_vault_is_opened_read_only_and_unchanged、bin/tests/integration_authz_audit.rs 的 integration_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_vault、integration_authz_audit 与 config/generic token 隔离矩阵全绿即为判据。
只读引导已在集成层重建,八条 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)
readonly_vault:RustyVault::new(backend, cfg),其中cfg.mounts_monitor_interval强制置 0。AC5 前半——「有没有 mounts monitor」是模式的决定,不是调用方配置的决定;un31_a_readonly_open_starts_no_background_worker的可写对照特意配了非零 interval,所以该断言说的是模式而不是配置。readonly_unseal:barrier.inited()→seal_config()→ 弃用分片检查(与常规 unseal 一致,读到一个被轮换掉的分片不是只读该做的让步)→ 自行ShamirSecret::combine(阈值为 1 时直接取分片)→barrier.unseal(kek)。绕开Core::unseal正是因为它会调私有的post_unseal。- 装配
CoreState:hmac_key/system_view/sealed = false。私有的kek、unseal_key_shares不装——它们只服务generate_unseal_keys(),那是写路径。 readonly_post_unseal:module_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 |
plan-20260820 的家族发布点。一次推送同时带上「删除 vendored」与「只读重建」——两者不可分割:中间任一状态单独上线,要么让只读审计失效,要么让 crate::vault 与 libvault 双存的语义含混。
兼容性证据:无对外 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.rs 的 mod 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 独立小卡承接。
执行期发现(已记入计划修订历史)
EX-03:libra 的提交签名要求 vault unseal key 可用,本环境不可用,本计划全部会推送的卡按 sign-off-only 提交(具名批准 = 计划所有者)。FIX-VLT-01:D 组 clippy 在 CI 的 stable 1.98.0 上有两条错误,本地 1.97.1 不触发——一条在 vendoredpki/util.rs(随 VLT-02 删除自动消失),一条在本仓自有src/api/router/lfs_router.rs。后者按 ER-10 另立卡并入本家族。该红在本计划开工前就存在(上一次 push 同一步骤已 failure),本次发布一并关闭。- 事实基线低估: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-smoke 在 v0.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 的顶层模块),提供VaultCore和VaultCoreInterface。src/contract/vault/integration/jupiter_backend.rs:将 RustyVault 的物理存储后端适配到jupiter数据库存储。src/jupiter/storage/vault_storage.rs:通过 SeaORM 读写vault表,提供list_keys、load、save、delete、delete_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.rs、src/contract/vault/nostr.rs、src/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 丢失场景下无法恢复。
| 能力 / 组件 | 实现状态 | 关键事实与风险 |
|---|---|---|
| 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 事件(含 operation、secret_name、outcome、caller),不包含 secret 值、root token 或分片;audit_secret_access 已 doc-comment 显式记录 fail-open 策略(审计经 infallible 的 tracing,绝不阻断 secret 操作);caller 身份经 tokio::task_local!(with_audit_caller)由入口点注入(config secret set/check/rotate、config 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)与 file(file_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_connection 和 config secret set/check 只依赖数据库和 vault key。 |
LoadMode / SecretRef / resolver |
已实现 | CLI 按命令选择加载级别;SecretRef/resolver 支持 mail.password_ref 延迟解析和缓存/evict。 |
本文档中以下约束是硬边界,任何实现偏离都必须重新评审:
- fail-closed 以
inited()为准,绝不在 key 缺失时清库。 DB 已初始化但core_key.json缺失时必须返回错误(VaultError::CoreKeyMissing,见vault_core.rs),绝不调用delete_all()——否则会静默销毁全部已存 secret;空 DB 无 key 仍允许首次初始化。 - 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 后台线程的捕获留白见「风险与约束」。 - 常规 secret 访问必须使用限权 token,不得使用 root token。 初始化时安装 ACL policy 并签发 ssh/pgp/nostr/pki/config/generic 限权 token;config token 与 generic token 必须 ACL 隔离(config token 不能读 generic secret,反之亦然),有矩阵测试。
- vault 运维命令必须使用最小 DB/Vault bootstrap。
config secret set/check、config vault reset/rekey/backup/restore只依赖数据库与 vault key 操作 secret;config validate --resolve-secrets会解析并校验完整配置(含 Redis、对象存储等字段的校验规则)并经 vault 解析 secret。它们都不得初始化 Redis 连接、对象存储后端、完整Storage或 HTTP 服务(不构造完整AppContext);缺数据库/vault 时给出明确前置提示而非 panic。 - 自动解封材料是静态保护边界,不能用“放进 vault”替代部署侧托管。 自动解封所需的 unseal 分片仍落在本地
core_key.json,能读取该文件的攻击者即可解封。生产高敏感部署必须配套 KMS/secret manager、受控挂载(Unix0700/0600)、备份恢复与恢复演练;备份恢复运行手册必须与 fail-closed 配套(否则 key 丢失从“自动重建”变成“无法恢复”)。 - KEK 轮换无 libvault 内建原语,不在本计划承诺。
init()后 KEK 不可变;unseal 分片 rekey(rekey_unseal_shares)可用,但彻底使旧分片失效需 KEK 轮换,须另立专项。 - 任何源码阶段都必须通过三项 gate。
cargo +nightly fmt --all --check、cargo clippy --all-targets --all-features -- -D warnings、source .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/check、config validate --resolve-secrets不能复用完整AppContext,必须使用最小 DB/Vault bootstrap。
任何试图在 Config::new 中读取 vault secret 的方案都不可行,因为 Config::new 是同步加载阶段,且此时 vault 还没有就绪。
事实校准(2026-06-24):本节为阶段 A 加固前的代码级分析快照,其中描述的“key 缺失即
delete_all清库”、“println!/log::debug!输出 root token”、“assert!触发 unseal”等行为已在阶段 A 消除(见本文件顶部“落地状态更新”与阶段 A 工作项)。当前VaultCore::configfail-closed、不再输出 root token、普通启动不再隐式清库。本节保留以记录历史决策脉络,新的实现以代码与阶段 A 描述为准。
本节是上面“启动依赖顺序”的代码级展开,所有步骤都标注了文件与行号,供实现与评审核对。启动分为两个阶段:同步阶段(尚未创建 tokio runtime,vault 不可能就绪)与异步阶段(service::exec 的 #[tokio::main] 启动 runtime 之后)。
【同步阶段 · 无 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 后续消费者之一
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
- vault 就绪点是唯一的
VaultCore::from_database_connection(context/mod.rs:60-71)。在它之前只消费 DB 连接;对象存储凭据在 vault 就绪后通过resolve_object_storage_secrets(context/mod.rs:75-76)解析,对象存储(build_object_storage,context/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::config的println!(: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 集成。
src/contract/vault/integration/vault_core.rs 当前在 key 文件不存在时会:
- 打印清库重建提示。
- 调用
vault_storage.delete_all()清空 vault 表。 - 重新初始化 RustyVault。
- 生成新的 root token 和 secret shares。
- 写入新的
core_key.json。
这意味着 core_key.json 丢失会导致所有 vault secret 被删除。集中更多凭据后,这会变成灾难性数据丢失路径。普通启动和 config secret check 必须改为 fail-closed:key 缺失时启动失败并提示恢复或显式 reset,不能自动清库。
当前测试 test_vault_reinitialize_after_file_loss 还固化了这一危险行为,后续需要改为验证 fail-closed。
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 使用 std::fs::File::create 创建,依赖系统默认 umask。生产前需要显式加固:
- Unix 下 vault 目录权限应为
0700或等价限制。 - Unix 下
core_key.json权限应为0600。 - 容器镜像、备份、日志采集必须排除该文件。
- 生产部署应优先考虑 systemd credential、Kubernetes Secret volume 或外部 secret manager 注入 key material,而不是长期保存普通明文文件。
即便完成权限加固,当前自动解封模式仍不能抵御能读取部署机磁盘的攻击者。安全收益应限定为“不进仓库、不进普通配置、不进日志”。
VaultCore::new / VaultCore::config 当前返回 Self,内部使用大量 expect、unwrap、assert。这让调用方无法区分:
- vault 目录创建失败。
- key 文件不存在。
- key 文件不可读。
- key 文件格式错误。
- RustyVault 创建失败。
- init 失败。
- unseal 失败。
- 数据库存储失败。
后续应改为返回 Result<Self, MegaError>,或引入 VaultError 后统一转换为 MegaError。普通服务启动、config secret check、config validate --resolve-secrets 都需要可诊断错误,而不是 panic。
JupiterBackend 当前接收完整 Storage,但实际只使用 ctx.vault_storage()。这让 vault bootstrap 被完整 Storage::new 绑定,而 Storage::new 会提前构造对象存储等能力。
这条接口边界太粗,直接阻碍 config secret set/check 独立运行。目标是拆出最小 DB/Vault bootstrap,只建立数据库连接和 VaultStorage 所需能力,不初始化 Redis、对象存储、HTTP、SSH、monorepo 或后台任务。
当前 interface 包含 token()、read_api、write_api、delete_api、read_secret、write_secret、delete_secret。普通调用方不应该知道 root token,也不应该直接拼 RustyVault API path。
后续应分层:
- vault 内部保留 raw API 能力。
- 业务调用方只使用规范化 secret name。
- 配置系统通过
SecretResolver使用SecretRef,不直接依赖 raw vault 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。
早期 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 风格路径。
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、结果如何”,不满足 Vault 生产加固的基本要求。需要启用审计设备(或在 interface 上统一审计 hook),记录每次 secret 读写的元数据,并对 secret 值做哈希 / 省略;审计日志本身不得泄露明文、root token 或分片(见阶段 H)。
计划目前只有破坏性 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 生产化迁移硬前置”的约束一致。
仅以“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()。
一旦阶段 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_secret → init_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_secrets → build_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.rs、vault/nostr.rs |
vault 内部 secret | 已由 vault 管理;读取、解析、保存和删除主路径已返回 Result,缺字段/坏格式不再 panic |
以下事项不是所有 vault 工作的统一硬前置。它们只阻塞依赖对应能力的阶段;vault 本地安全止血、最小 bootstrap、interface 收窄、消费端 panic 清理可以先行推进。
现状提示(2026-06-15):经核查,下列三项前置在当前仓库均尚未实现。但它们只阻塞依赖各自前置的阶段(A 的脱敏部分、D、E、G)——存在一条不依赖它们的 vault-only 执行链(A 止血子集 → B → C → F),详见下文《可执行性评估》。
落地状态(2026-06-19):已在
src/config/redaction.rs落地Redactortrait +UrlRedactor+global_redactor()+redact_db_url/redact_redis_url/redact_url,并接入真实调用点:DB 连接日志(jupiter/storage/init.rs用redact_db_url)、Redis 连接日志(jupiter/redis/mod.rs用redact_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.rs 或 src/config/redaction.rs(已采用 src/config/redaction.rs)
职责范围:
- 脱敏 URL(移除密码和关键参数)
- 示例:
postgres://user:password@host:5432/db→postgres://***:***@host:5432/db
- 示例:
- 脱敏 token 和密钥
- 示例:
secret_abc123xyz→secret_***
- 示例:
- 脱敏 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反序列化失败的错误信息 VaultError的Display实现中:所有敏感字段都应经过脱敏
前置条件:
- 只阻塞“需要保留但必须脱敏的日志/错误输出”、跨模块脱敏测试与 SecretRef 生产化 gate。
- 不阻塞阶段 A 中“删除 root token / 分片 / key 文件内容输出”的核心止血;这部分应直接删除敏感输出,避免先构造再脱敏。
- 可作为独立前置工作先完成
- 不被任何其他阶段阻塞
验收标准:
- 脱敏工具能被 config、vault、mail、notification 等模块导入使用
- 单元测试覆盖所有脱敏类型
- 脱敏后的输出不包含明文敏感信息(token、密码、shares 等)
目标:与 config.md 阶段 2 协同设计 LoadMode 框架,为两个模块都需要的 CLI 改造提供统一的设计。
设计范围:
- 定义
LoadModeenum(所有可能的加载级别) - 明确各模式的启动路径和依赖
- 定义命令与加载模式的映射关系
期望产出:
- 共同设计文档或 RFC
src/cli/load_mode.rs或等价的设计代码框架
前置条件:
- 阶段 D(P1)需要与 config 阶段 2 协同完成此设计
- 不能分别实施,否则会出现不一致
关键约束:
- 阶段 B(最小 bootstrap)应在 config 阶段 3 之前完成,或至少同期进行
- config 阶段 3 直接依赖 vault B 的改造结果
- 两个团队应协商明确的交付顺序和时间表,避免 config 因等待 vault 而延期
本节按"当前代码与依赖的实际状态"核查各阶段能否立即执行,仅调整可行性与排期,不改动代码。核查基于 mega2 当前 src/ 与 src/vault 中 vendored 的 libvault。
可执行度总评:中高(7/10)。 vault 自身的 P0/P1 加固路径具备落地条件,主要障碍不是技术不可行,而是文档后半部分仍沿用旧前置口径:把"删除敏感输出"错误地写成必须等待 redaction,把"最小 bootstrap"与"LoadMode/SecretRef"混在同一阻塞链里。执行时必须把工作拆成两条队列:
- vault-only 队列:A 核心止血 → B → C → F,可立即开工;H、I 在 C 之后接入,不需要等待
LoadMode或SecretRef。 - 跨 config 队列:redaction、
LoadMode、SecretRef、对象存储后置初始化,按config.md对应阶段协同推进;这些工作不应反向阻塞 A 核心。
- 前置依赖口径需保持收敛:A 核心不依赖 redaction;文档中任何把“删除敏感输出”写成“必须等待脱敏工具”的表述都应删除或降级为“保留输出时才需要脱敏”。否则会让可以立即落地的安全止血被误判为阻塞。
- 阶段颗粒度不够执行化:A 同时包含删除输出、错误模型、fail-closed、权限、reset 命令设计,实际应拆成"A1 直接止血"、"A2 初始化语义"、"A3 权限与测试"三个可评审单元。
- 跨模块工作与本地工作混排:D/E/G 的确依赖 config,但 B/C/F/H/I 不依赖 config 交付,优先级表应显式分流。
- 验收标准可测,但缺少入口清单:已有验收项大多具体,但缺少"第一批 PR 改哪些文件、不能改哪些范围"的执行边界。
- 仓库格式容易误判:当前工作副本使用 Libra 管理,
git diff失败不是代码或文档不可执行的证据。执行者应先用libra status确认已有改动,再用libra diff -- docs/refactoring/vault.md或对应路径核对差异,避免误操作其他人已有改动。 - 代码定位应以符号为准:当前行号有轻微漂移,执行时应以
VaultCore::config、JupiterBackend::new、VaultCoreInterface等符号定位,行号只作辅助。
-
三项跨模块"硬前置"在当前仓库尚不存在:
- 日志脱敏工具(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/ref、SecretRef、config validate --resolve-secrets:未实现。- 凡声明依赖这三者的阶段(A 的脱敏部分、D、E、G),若不先交付前置,无法照原文执行。
- 日志脱敏工具(config 0b):
-
libvault 内建治理路径开箱可达(embedded API:
init()→unseal()后用 root token 即可write/read):sys/policy/{name}、sys/policies/acl/{name}、auth/token/create、secret/*均为默认挂载(src/vault/mount.rs:45-64、modules/auth/mod.rs:38-48、core.rs:609-632的post_unseal)。→ 阶段 I 的 policy/token 不需要额外挂载 plumbing,mega2 侧即可落地。阶段 H 不依赖这些内建路径,应在 mega2 的收窄 interface 上实现 hook。 -
fail-closed 判定依赖的
rvault.core.load().inited()存在且测试已用——阶段 A 第 5 项可直接执行。 -
当前工作副本是 Libra 格式仓库;工作区检查应使用
libra status,差异检查应使用libra diff -- <path>。这只影响开发/评审工作流,不改变 vault 代码的技术可执行性。 -
行号轻微漂移:本次迁移合并了
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 命令) | ✅ 已实现 | LoadMode、config secret ref/set/check、config validate --resolve-secrets 已落地;vault 命令使用最小 DB/Vault bootstrap。 |
| E(SecretRef + mail.password_ref) | ✅ 已实现 | SecretRef / resolver / mail.password_ref 已落地,明文 password 与 password_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、配置解析和业务消费端,建议按以下可回滚切片推进:
- A1 直接止血:删除 root token / 分片 / key 文件内容输出,避免新增脱敏依赖;只改初始化路径和对应测试。
- A2 初始化语义:
VaultCore::new/config返回Result,fail-closed 以 DB 初始化状态为准,移除普通启动中的delete_all()数据丢失路径。注意:此步需同步处理AppContext::new及直接调用点的错误传播(属于允许的最小波及,见"下一步建议"执行边界)。 - A3 key material 权限:Unix 权限
0700/0600与权限断言测试;非 Unix 平台只验证不放宽现有行为。 - B 最小 bootstrap:拆
JupiterBackend的 storage 边界,但不新增config secret命令。 - C/H interface 与审计入口:先隐藏 token/raw path,再挂审计 hook;审计目的地与 fail-open/fail-closed 策略须在实现前明确。
- 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 三大门禁验证。
- 先加固 vault,再迁移配置 secret。
Config::new只解析SecretRef,不读取真实 secret。- secret 真实值解析必须发生在 vault 就绪之后。
config secret set/check必须使用最小 DB/Vault bootstrap。- 引导配置和早期运行时依赖不进入本项目 vault。
- root token、secret shares、明文 secret、数据库 URL 密码、Redis URL 密码都不得进入日志或错误信息。
- 破坏性 reset 必须是显式运维动作,不能隐藏在普通启动中。
- fail-closed 必须与备份恢复方案配套;引入 fail-closed 前后,key 丢失(数据在)必须有文档化的恢复或安全重置路径。
- 最小权限优先:root token 仅用于初始化,常规运行通过限权 token;调用方不接触 root token,也不直接拼 raw path。
- secret 访问可审计:read/write/delete 留下不含明文的审计记录。
- 每个阶段都应独立可编译、可测试、可回滚。
阶段依赖声明(2026-06-15 修订):本计划不再把所有阶段统一挂到同一组跨模块前置上。执行时按以下边界处理:
- vault-only 阶段:A 核心、B、C、F 可在当前仓库直接执行;H、I 在 C 之后执行;这些工作不等待 redaction、
LoadMode或SecretRef。- redaction 依赖:只阻塞“需要保留但必须脱敏的日志/错误输出”、跨模块脱敏测试,以及后续 SecretRef 生产化 gate;不阻塞 A 核心中“删除敏感输出”的工作。
- CLI LoadMode 依赖:只阻塞阶段 D 的
config secret命令族和validate --resolve-secrets,应与config.md阶段 2 共用同一框架。- SecretRef 依赖:阶段 E 必须等待
config.md阶段 5 与mail.md阶段 2;在此之前不得把mail.password_ref列入可交付范围。- 阶段 B 的同步要求:最小 DB/Vault bootstrap 必须在
config.md阶段 3 之前完成,或与其同期交付,因为 config 的 secret 命令直接依赖该能力。
目标:在迁移任何配置 secret 前,消除最危险的泄露和数据丢失路径。
前置依赖(2026-06-15 修订,见《可执行性评估》):工作项 1-2"停止输出 root token / 分片 / key 文件内容"不需要脱敏工具——直接删除
println!/log::debug!、不构造含密字符串即可,现在可立即执行。脱敏工具(config 0b)只在"需保留输出的含密日志(如 DB/Redis URL)做部分脱敏"时才需要;该部分与"止血"解耦,不阻塞本阶段核心。
工作项:
- 删除 root token 的 stdout、stderr、tracing 输出。
- 删除 secret shares 和完整 key 文件内容的任何输出。
VaultCore::new/VaultCore::config改为返回Result<Self, _>,引入专门的VaultError(区分目录创建、key 缺失、key 不可读、格式错误、init、unseal、存储失败),并统一转换为MegaError。VaultError与其Display不得嵌入 root token、分片或 secret 明文;不要用MegaError::Other(format!(..))直接拼接敏感值。- 替换初始化路径上的
expect、unwrap、assert。 - fail-closed 判定以 DB 是否已初始化(
rvault…inited())为准,而非 key 文件是否存在:DB 已初始化但 key 文件缺失时启动失败、绝不delete_all();仅当 DB 未初始化且无 key 文件时才允许合法首次初始化。 - 显式 reset/init 作为后续运维命令设计,不能由普通启动隐式触发。
- Unix 下创建 vault 目录时设置
0700,创建core_key.json时设置0600。 - 将
test_vault_reinitialize_after_file_loss改为验证 key 缺失不会清空 vault 数据。
验收标准:
- 启动和测试日志中不出现 root token(应有捕获日志的测试断言 token 与分片不出现)。
- 全新(空 DB)环境仍可完成首次初始化;DB 已初始化但删除
core_key.json后,普通启动失败且 vault 表数据不被清空。 - 初始化失败返回可诊断错误,不 panic,且错误信息不含 token / 分片 / 明文。
- key 文件和目录权限符合最小可读写范围。
目标:让 vault 运维命令不依赖完整 AppContext。
与 config.md 的强绑定(2026-06-14 更新):本阶段的改造结果是 config.md 阶段 3 的直接依赖。config 需要基于本阶段拆出的最小 bootstrap 能力来实现
config secret set/check等命令。因此本阶段应在 config.md 阶段 3 之前完成,或至少同期进行,以避免 config 因等待而延期。
工作项:
- 将
JupiterBackend从依赖完整Storage改为依赖最小 vault storage 接口边界。 - 新增
VaultBackendStorage或等价接口,只覆盖list_keys、load、save、delete。 - 让
VaultStorage成为生产 adapter。 - 新增 DB-only / Vault-only bootstrap 能力,只建立数据库连接和
VaultStorage。 - 确保该 bootstrap 不初始化 Redis、对象存储、HTTP、SSH、monorepo、后台任务。
验收标准:
- 可以只凭数据库配置构造
VaultCore。 config validate --resolve-secrets的底层 bootstrap 不依赖 Redis/S3。- vault 集成测试不需要完整服务上下文。
目标:隐藏 token、raw API path 和路径拼接规则,减少调用方误用。
工作项:
- 将 raw
read_api/write_api/delete_api限制在 vault 内部。 - 普通业务调用方只使用相对 secret name。
- 引入
SecretName或等价校验,禁止以/开头,禁止带secret/前缀。 - 错误信息只输出 redacted path,不输出 secret 明文。
- 逐步让 SSH、PGP、Nostr、PKI 调用点使用收窄后的 interface。
验收标准:
- 普通调用方无法访问 root token。
secret/secret/...这类路径重复可以在入口被拒绝。- 错误信息不包含明文 secret。
目标:为 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 都基于这个共同框架来实现各自的子命令
- 避免分别实施导致的设计不一致或集成冲突
工作项:
- 先在 CLI 层支持两阶段加载和
LoadMode(与 config.md 阶段 2 协同完成):在共同的LoadMode设计框架下,改造 CLI 分发逻辑以支持不同的启动模式。 - 新增不依赖 vault 的
mega2 config secret ref。 - 基于最小 DB/Vault bootstrap 新增
config secret set。 - 基于最小 DB/Vault bootstrap 新增
config secret check。 - 新增
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 参数、日志或错误信息。
目标:让配置文件保存 secret 引用,而不是保存可迁移凭据明文。
首批字段:
mail.password->mail.password_ref
工作项:
- 在配置模块中定义
SecretRef。 - 定义
SecretResolvertrait。 - 实现
VaultSecretResolveradapter,内部调用VaultCoreInterface::read_secret。 - resolver 将
vault://secret/config/prod/mail/password#value映射为read_secret("config/prod/mail/password"),再读取value字段。 mail.password和mail.password_ref迁移期互斥:同时存在为 hard error。- 消费端在 vault 就绪后通过 resolver 获取 SMTP 密码。
- resolver 提供缓存 TTL 和
evict/evict_all。
验收标准:
mail.password_ref可解析并用于 SMTP mailer。- secret 缺失、字段缺失、引用格式错误均返回可诊断错误。
- 错误和日志只出现脱敏引用,不出现明文密码。
database、redis、object_storage.s3.*没有被错误迁移为SecretRef。
目标:让 vault 内部 secret 数据损坏时可诊断、可恢复,而不是直接 crash。
优先处理:
- 已完成:
src/server/ssh_server.rs的 SSH server key 读取、生成、写入失败返回错误。 - 已完成:
src/contract/vault/pgp.rs的 PGP key 读取、解析、保存、删除返回Result。 - 已完成:
src/contract/vault/nostr.rs的 Nostr key 读取、生成、解析返回Result。 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 明文。
目标:只有在确实需要让对象存储凭据进入 vault 时,才重构完整初始化顺序。
2026-06-17 当前决策:本轮不迁移
object_storage.*凭据……(历史决策,见下)2026-06-27 落地:已实现分阶段 bootstrap,
object_storage.s3.access_key_id/secret_access_key现可配置为vault://SecretRef。AppContext::new(src/context/mod.rs)只建一次 DB 连接,先做 DB-onlyVaultCore::from_database_connectionbootstrap(不需要完整Storage),再经resolve_object_storage_secrets解析对象存储凭据中的 SecretRef,最后用解析结果build_object_storage并Storage::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 的操作不依赖对象存储可用。
目标:让 vault secret 的访问可追溯,满足集中凭据托管的审计要求。
新架构修订(2026-06-15):libvault 的内建审计设备不可用——
sys/audit、sys/audit/{path}路径虽已注册,但 handler 全部是桩实现(返回Ok(None),见modules/system/mod.rs:883-905)。因此本阶段只走 interface hook 路线,不依赖内建审计设备(除非愿意先补实现 libvault 的这些 handler)。审计 hook 与阶段 C 强耦合,应作为阶段 C 收窄VaultCoreInterface的产物之一。
工作项:
- 在收窄后的
VaultCoreInterface上增加统一审计 hook(不要依赖 libvault 内建审计设备,理由见上)。 - 记录每次 read / write / delete 的调用方、规范化 secret name、时间、结果(成功 / 失败 / 未命中)。
- 审计记录对 secret 值做哈希或省略,绝不落明文;root token、分片不进入审计。
- 显式决定并记录审计写入失败的策略(fail-open 还是 fail-closed)。
已完成首批(2026-06-19):第 4 项已落地——
VaultCore::audit_secret_access(src/contract/vault/integration/vault_core.rs)已补 doc-comment 显式记录fail-open策略及其理由:审计经tracing(infallible)发出,secret 操作绝不因审计步骤被阻断/失败,这是可用性优先于不可否认性的刻意选择;该 target 仅记录 name + outcome,天然不含明文/root token/分片。配置化首批(2026-06-23):审计现在可配置且默认开启。新增
config.vault.audit.enabled(VaultAuditConfig,默认true);组合根AppContext::new经VaultCore::with_audit_config(config.vault.audit)注入,audit_secret_access在enabled = 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_access已Result化并由 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 仍为后续。)
目标:从“全程 root”过渡到按 policy 限权,符合 Vault root token 生命周期标准。
新架构修订(2026-06-15):libvault 已内建可直接接入的原语,本阶段从"自建授权体系"改为"接入并编排内建能力":
- ACL policy:
modules/policy,sys/policy/{name}与sys/policies/acl/{name},capability 含deny/read/write/list/sudo/create/delete等,支持 HCL。- 非 root token:
modules/auth/token_store.rs,auth/token/create支持 policy 子集校验、ttl/explicit_max_ttl/period/num_uses,并有auth/token/revoke[-orphan]、renew与后台ExpirationManager(expiration.rs)做租约过期。
工作项:
- 通过
sys/policy/{name}(或sys/policies/acl/{name})按 secret 前缀定义 ACL policy(ssh、pgp、nostr、pki、config/*各自最小权限)。 - 通过
auth/token/create为各消费端签发带对应 policy、受限 TTL 的非 root token,替换直接使用 root token 的路径;单一 token 泄露的影响面由其 policy 子集界定。 - 初始化后撤销常驻 root token(
auth/token/revoke/{id})。注意:当前 libvault 公开 API 没有 Vault 式sys/generate-root在线重建仪式(root token 仅在init()时产生)。因此"需要时临时重建 root 权限"必须依赖阶段 J 的恢复托管(安全保存初始 root token,或预置一个具 root policy 的恢复 token),而不能依赖在线 generate-root。 core_key.json不再长期保存可用 root token,仅保留恢复所需的分片(与阶段 J 的备份方案配合)。
验收标准:
- 常规运行链路不持有可用 root token。
- 任一消费端 token 泄露只影响其 policy 覆盖的 secret。
- 仍可通过显式运维流程重建 root 权限执行管理操作。
目标:让泄露和 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 轮换,验收标准也不应包含它。
工作项:
- ✅ 提供 unseal 分片 rekey 的运维命令(基于
generate_unseal_keys()/unseal_once())——已落地config vault rekey --force [--key-path](见下文“重新生成 unseal 分片”与“已完成(2026-06-27)”)。vault 加密 key(KEK)轮换因无内建原语,单列为后续专项,不在本阶段交付(见上)。 - ✅ 定义密钥材料(分片 / 恢复凭据)的安全托管与备份位置(外部密钥管理系统 / 离线托管),写入下文“Vault 恢复运行手册”。可执行入口已落地:
config vault backup <DESTINATION> [--key-path <PATH>]把core_key.json复制到目标位置并生成.meta.json元数据;备份文件在 Unix 下权限设为0600。 - ✅ 定义“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。 - 为可迁移 secret(首批
mail.password)提供轮换支持。 5.(可选,长期)评估外部 KMS / transit auto-unseal,替代本地落盘自动解封,缓解磁盘读取威胁。
已完成首批(2026-06-19):第 4 项已落地——新增
mega2 config secret rotate <field> --vault-path ... --field ... --value-stdin(src/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.rs的exec_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_mode、config_vault_rekey_requires_force、config_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.rs的exec_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_mode、config_vault_backup_accepts_key_path、config_vault_restore_uses_vault_bootstrap_load_mode、config_vault_restore_requires_force)与核心功能单测(test_backup_key_creates_key_and_meta_file、test_restore_key_verifies_and_replaces_key_file、test_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)。
本运行手册记录阶段 A/J 加固后应遵循的 Vault 运维行为,重点覆盖 core_key.json 备份、恢复、显式重置、unseal 分片重新生成,以及 root token 恢复材料的边界。
core_key.json保存嵌入式 RustyVault 实例的本地自动解封密钥材料。- 数据库
vault表保存加密后的 Vault 数据。 - 当数据库已经初始化但
core_key.json缺失时,服务启动必须故障关闭(fail-closed);普通启动不得删除vault表数据。
-
使用运维命令将
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权限。 -
将备份副本加密保存到应用主机之外的外部密钥管理系统、离线加密介质,或等效的受限凭据系统中。
-
除非快照本身已加密并有访问控制,否则
core_key.json及其备份必须排除在容器镜像、日志采集、源码控制、支持包和普通文件系统快照之外。 -
Unix 环境下,保持 vault 目录权限为
0700,core_key.json与备份文件权限为0600。
-
停止 mega2。
-
使用恢复命令将验证过的备份 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 文件保持不变,命令返回错误。 -
手动设置权限(restore 已在 Unix 下将新 key 文件设为
0600,仍需确认目录为0700):chmod 700 "$(dirname "$CORE_KEY_PATH")" chmod 600 "$CORE_KEY_PATH"
-
启动 mega2。
-
确认依赖 Vault 的消费者可以正常读取 secret。
如果没有匹配的密钥材料,按当前嵌入式 RustyVault 设计,已加密的 Vault 数据无法恢复。不要期望服务启动后自动重新初始化;它必须故障关闭(fail-closed)。
仅当丢失所有 Vault secret 可以接受时,才允许使用本流程。
- 停止 mega2。
- 备份数据库和任何现存的
core_key.json。 - 在受控维护流程中删除
vault表数据,或重建数据库。 - 删除旧的
core_key.json。 - 启动 mega2,使其针对未初始化的 vault store 执行首次初始化。
- 重新创建必要的 secret。
普通服务启动绝不能隐式执行这个重置。
运维命令:mega2 --config <path> config vault rekey --force [--key-path <PATH>](src/commands/config.rs 的 exec_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 原语能力。
Rust 应用接口不再向普通调用方暴露 root token,secret 操作通过收窄后的 secret interface 和审计 hook 执行。当前本地自动解封文件仍保存兼容与恢复所需的 root 恢复材料。要安全移除这部分材料,必须另行设计 root recovery token 或外部凭据托管机制;如果没有恢复路径就直接移除,未来维护可能变得不可执行。
| 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 或同等初始化顺序重排 | 无 | 无 |
关键同步点:
- 日志脱敏工具(来自 config 0b)→ vault 中需要保留的敏感上下文输出:不阻塞 A 核心止血;阻塞跨模块统一脱敏、错误诊断脱敏和 SecretRef 生产化 gate。
- vault B 完成 → config 3 依赖:config 必须等待 vault 的最小 bootstrap 拆分,或与其在同一改造中交付。
- CLI LoadMode 框架(config 2 与 vault D 协同):两个文档需共同设计而非分别实施。
- config 5 + mail 2 → vault E:
SecretRef和mail.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 |
涉及文件:
src/contract/vault/integration/vault_core.rssrc/contract/vault/pgp.rssrc/contract/vault/nostr.rssrc/server/ssh_server.rs
问题:调用方需要知道 token、path、JSON shape、字段名和错误行为,interface 过宽且接近 implementation。
方案:让 VaultCore 对外只提供规范化 secret 操作,业务模块通过 typed helper 读取自己的 secret。path 映射、redaction、错误分类集中在 vault module 内部。
收益:调用方 interface 更小,测试 surface 更清晰,secret shape 变更的 locality 更好。
涉及文件:
src/contract/vault/integration/jupiter_backend.rssrc/jupiter/storage/vault_storage.rssrc/context/mod.rs
问题:vault 只需要 vault table,却依赖完整 Storage 生命周期。
方案:引入最小 VaultBackendStorage 接口边界,让 VaultStorage 成为 adapter。JupiterBackend 不再知道完整 Storage。
收益:config secret 命令不被 Redis、S3、monorepo 初始化阻塞;vault 测试更容易隔离。
涉及文件:
- 未来
src/config/secret.rs src/contract/vault/integration/vault_core.rs
问题:VaultCore 管 secret 存储,SecretRef 管配置引用,两者语义不同。
方案:在 config module 定义 SecretRef 和 SecretResolver,由 VaultSecretResolver 作为 adapter 连接 vault。
收益:Config::new 继续只做同步配置解析,secret 解析作为 vault 就绪后的独立异步阶段,符合启动依赖顺序。
涉及文件:
src/cli.rssrc/commands/mod.rs- 未来
src/commands/config.rs
问题:当前 CLI 在分发任何子命令前会加载完整配置,完整 AppContext 又会初始化过多依赖。
方案:先实现两阶段 CLI 和 LoadMode,再让 config secret 选择 VaultBootstrap,让 service 选择 FullAppContext。
收益:无配置、坏配置、缺 Redis/S3 时仍能执行配置诊断和 secret 运维。
涉及文件:
src/contract/vault/pgp.rssrc/contract/vault/nostr.rssrc/server/ssh_server.rssrc/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 切片):
VaultCore::new/VaultCore::config改为返回Result,引入不泄敏的VaultError。- 删除 root token、分片、key 文件内容的所有输出(stdout / stderr / tracing)。
- fail-closed 以
rvault…inited()为准:DB 已初始化但 key 缺失即失败、不清库;空 DB 无 key 仍可合法首次初始化。 - key 文件和目录创建时设置权限(目录
0700、core_key.json0600)。 - 把
test_vault_reinitialize_after_file_loss改为:DB 已初始化时缺 key 不清空数据,并新增空 DB 首次初始化用例。 - 更新调用方(至少
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 build与cargo 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 / 交付说明中标明未触碰。
这一步完成前,不建议开始 SecretRef、mail.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 捕获。
- 影响:stdout/
- 约束:KEK 轮换无 libvault 内建原语。
- 理由:
init()后 KEK 不可变,无sys/rotate等价能力。 - 影响:彻底使旧 unseal 分片失效需另立 KEK 轮换专项;本计划只承诺分片 rekey。
- 理由:
- 约束:审计 fail-open 为默认。
- 理由:审计经 infallible 的
tracing不阻断 secret 操作(可用性优先)。 - 影响:需要 non-repudiation 的部署应显式设
config.vault.audit.fail_closed = true(仅对filesink 生效)。
- 理由:审计经 infallible 的
| 维度 | 评估结论 |
|---|---|
| 合理性 | 高(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/rekey、config 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 轮换边界已明确,避免过度承诺。