# 隔离备份恢复演练证据 ## 证据边界 - 仅记录日期、角色、短构建标识、状态、finding 类别和清理结果。 - 不记录远端地址、SSH 身份、完整目录、凭据、账号、原始 manifest、业务字段或附件内容。 - 腾讯云是 OA 生产;ARM 仅是异机备份、外部健康探测和隔离恢复演练节点。 ## Attempt 1:前置门禁失败关闭 日期:2026-08-23 | 检查项 | 结果 | | --- | --- | | 生产公网 `/ready`(演练前) | `PASS` | | 计划基线构建短标识 | `7b86ffb` | | 最新 ARM 正式日批次选择 | 已选择最新候选,未降级到旧批次 | | manifest 来源构建门禁 | `FAIL`:`build_mismatch` | | 失败阶段 | `select_latest_batch` | | finding 类别 | `latest_batch_invalid` | | 数据库/附件复制 | 未开始 | | 一次性 PostgreSQL / 恢复写入 | 未创建、未执行 | | runtime 清理 | `PASS` | | 临时 root 入口清理 | `PASS` | | 结论 | `NO_GO` | 该结果只证明最新候选未通过来源构建门禁,不能单独证明备份内容损坏。首次失败证据必须保留,禁止自动改选更旧批次或放宽门禁。 ## 失败后只读复核 日期:2026-08-23 | 检查项 | 结果 | | --- | --- | | 生产公网 `/ready` | `PASS`,构建短标识 `7b86ffb3` | | 生产自动备份 timer | `active` | | 生产自动备份 unit 冻结构建 | `7b86ffb3` | | ARM dead-man timer | `active` | | ARM 公网健康 timer | `active` | | ARM 一次性 root 入口残留 | `0` | | ARM 本轮 runtime 残留 | `0` | | ARM 普通用户暂存候选 | 窄名称初扫发现 `1`;随后按本轮 5 个文件的 SHA-256 精确匹配并全部删除 | 只读复核排除了“当前生产构建事实源过期”和“生产自动备份 unit 仍冻结旧构建”两种解释。ARM 上本轮 5 个普通用户暂存文件均经 SHA-256 与本地候选逐一匹配后精确删除,未执行宽泛目录删除。剩余调查重点是:最新 ARM 正式日批次是否来自当前生产自动备份链,以及 manifest 构建身份在生产生成、异机传输或 ARM 日归档选择中的哪一环发生漂移。 ## Attempt 1 当时判定与重试门禁 - Attempt 1 仍为 `NO_GO`,当时 AC1、AC3、AC4、AC5 未满足;该次没有形成数据恢复 RTO/RPO 实测值。 - 不重试恢复,不触碰 ARM 正式归档,不修改腾讯云生产数据或配置。 - 下一步只允许以窄权限读取以下脱敏事实:最新 ARM manifest 的构建短标识、批次时间与最新生产成功状态是否指向同一 run id、manifest 摘要是否一致。 - 若确认只是 ARM 最新批次来源/归档选择异常,另行规划修复备份链;若确认归档身份正确且仅演练器口径有误,先更新任务设计并重新批准。 - 任何第二次恢复尝试前,必须更新规划、重新生成一次性入口并再次取得用户明确批准。 ## 只读身份探针结果 日期:2026-08-23 | 检查项 | 结果 | | --- | --- | | 最新 ARM manifest 构建短标识 | `ec682565` | | 当前生产/自动备份 unit 短标识 | `7b86ffb3` | | 构建匹配 | `NO` | | 最新批次年龄 | 约 18 小时,`within_24h=yes` | | run id 与目录配对 | `PASS` | | DB/附件 manifest 大小与 SHA-256 | `PASS` | | source image digest | 存在 | | manifest 摘要 | 已取得 12 位脱敏前缀,不保存完整摘要 | | 探针临时 root 入口清理 | `PASS`,残留 `0` | | 普通用户暂存清理 | 3 个候选均按 SHA-256 精确删除,目录残留 `0` | | ARM 两个监控 timer | `active` | `ec682565` 是仓库可识别的历史提交,也是当前生产提交的祖先。随后只读对时确认:当前生产自动备份合同约 1.3 小时前才刷新,而最新 ARM 批次约 18 小时前生成;下一次自然调度约 14 小时后执行。因此最新 ARM 批次是在当前发布之前由当时活动构建生成,不支持“错误来源批次”或“传输损坏”的判断。 当前 finding 应归类为“发布后尚未产生新构建恢复点”。首次恢复入口按当时已批准的精确构建门禁正确失败关闭;该 `NO_GO` 是 Attempt 1 的真实历史结论,不覆盖也不回写。后续设计复盘确认,严格相等不是原始备份可恢复性的必要条件,新的准入合同见 2026-08-24 规划修订。 当时用户决策:不手工触发生产自动备份,等待既有 timer 自然生成并异机归档下一批恢复点。该决策不是完成项,也不永久排除手工备份;若后续自然批次异常,可重新作为需明确批准的备选项。 ## 2026-08-24 设计复盘与有效产出 - 自 2026-08-23 对时到当前日期,下一次自然调度窗口已经过去;当前唯一可立即执行的动作是按新设计重新只读探测 ARM 最新批次,而不是继续等待。 - Attempt 1 已经观测到:生产合同刷新后,到下一个自然备份抵达前,没有与当前构建严格相等的异机恢复点。该“同源恢复点可用滞后”是独立于数据 RPO/RTO 的有效恢复画像,即使数据库恢复未开始也必须保留。 - `ec682565` 与 `7b86ffb3` 的 migration 目录均为 65 个,目录差异为 0;这证明本次历史批次与当前 schema 没有 migration 增量,但不能证明“65”适合作为长期硬编码断言。 - 修订后的恢复点门禁为:manifest 来源提交是当前生产构建的已知祖先或相同,批次年龄不超过 24 小时,且 manifest/两份产物完整。严格相等只用于度量同源恢复点何时形成。 - migration 验证改为来源提交目录名称集合与恢复库成功集合精确一致;来源/当前集合不同则把“升级到当前 schema”单独标为未验证,不在本轮临时执行 migration。 ## 2026-08-24 最新自然批次只读探针 本节仅追加,不改写 Attempt 1 及 2026-08-23 既有证据。 | 检查项 | 结果 | | --- | --- | | 演练开始时公网 `/ready` | `PASS` | | 公网当前构建唯一解析 | `PASS`,短标识 `7b86ffb3` | | 生产自动备份 unit 与公网构建 | `MATCH`,timer `active` | | ARM 最新批次构建短标识 | `7b86ffb3` | | 来源与当前构建关系 | `SAME`;因此同时满足“祖先或相同” | | 批次年龄 | `68491` 秒,约 19 小时 2 分,`within_24h=yes` | | run id 与目录配对 | `PASS` | | DB/附件 manifest 大小与 SHA-256 | `PASS` | | source image digest | 存在 | | manifest 摘要 | 已取得 12 位脱敏前缀,不保存完整摘要 | | 来源/当前 migration 目录 | 均为 65 个,差异文件 `0` | | 探针临时 root 入口 | 自清理 `PASS`,复核残留 `0` | | 普通用户暂存 | 3 个候选按 SHA-256 精确删除,目录残留 `0` | | ARM 两个监控 timer | `active` | 同源恢复点画像:生产备份合同刷新距探针约 `94465` 秒;最新同源 manifest 距探针 `68491` 秒,因此该同源批次的 manifest 创建时间约在合同刷新后 `25974` 秒(约 7 小时 13 分)。当前证据没有直接采集 ARM 原子归档完成时刻,不能把该差值表述为精确“到达 ARM”耗时;只能证明同源批次创建滞后约 7 小时 13 分,并且在本次探针时已经可用。 执行方结论:最新自然批次满足修订后的来源关系、新鲜度、run id 配对和产物完整性门禁,可进入恢复重试审批。该结论不是恢复授权;本次只读探针没有复制或解包备份、创建 Docker 对象、恢复数据库/附件或修改生产与 ARM 正式归档。 ## 2026-08-24 同源滞后锚点更正 本节追加说明前节 `25974` 秒的锚点,不改写既有记录。 - 起点实际是生产 `automatic-backup` service 构建 drop-in 的最大文件 mtime;终点是最新 manifest `createdAt`。 - 该差值可称为内部诊断指标“合同 drop-in 文件刷新到同源 manifest 创建”,不能称为应用发布完成到恢复点到达 ARM 的精确耗时。 - 当前证据没有采集 ARM 原子归档完成时刻,因此正式 `same_build_recovery_point_lag` 只能保留为“存在发布后空窗”的定性 finding,`7h13m` 不对外引用为精确恢复画像。 - 2026-08-23 “下一次自然调度约 14 小时后”的估算存在时区处理错误:探针把 systemd 输出的本地时区时间直接附为 UTC,导致约多算 8 小时。修正后自然调度约在当时探针后 6 小时,与 2026-08-24 最新 manifest 创建时间一致;没有证据表明中间发生非计划手工备份。 ## Attempt 2:临时副本目录身份误用 日期:2026-08-24 本节仅追加,不改写 Attempt 1、只读探针或锚点更正记录。 | 检查项 | 结果 | | --- | --- | | 最新正式批次前置门禁 | `PASS` | | 正式归档读取/修改 | 只读使用、零修改 | | 临时副本创建 | 已复制到本轮私有临时区 | | 失败阶段 | `create_isolated_runtime` | | finding 类别 | `copied_batch_invalid` / `batch_identity` | | PostgreSQL 容器 | 未创建 | | DB/附件恢复 | 未开始 | | migration/关键表/附件关联验证 | 未执行 | | runtime 清理 | `PASS`,复核残留 `0` | | 临时 root 入口清理 | `PASS`,复核残留 `0` | | 普通用户暂存清理 | 6 个候选按 SHA-256 精确删除,目录残留 `0` | | ARM 两个监控 timer | `active` | | 结论 | `NO_GO` | 根因:runner 的 `validate_batch` 同时承担正式源目录身份和批次内容校验。正式源目录 basename 必须与 manifest `runId` 配对是正确安全门禁;复制后的私有临时目录固定名为 `batch`,内容仍保持同一 manifest/run id,却被同一个 basename 断言错误拒绝。该 finding 不表示备份损坏,也没有进入数据库或附件恢复。 修订边界:正式源目录继续执行 basename/run id 强校验;临时副本只跳过目录 basename 断言,manifest、run id 格式、文件集、构建、时间、大小、SHA-256 和与源元数据相等比较全部保留。任何重试前必须补“正式源通过、同内容改名副本通过、任一内容篡改失败”的合成回归并再次取得明确批准。 ## 2026-08-25 Attempt 2 finding 分类补充 本节只追加分类,不改写 Attempt 2 原始结果。 - Attempt 2 finding 类别明确为 `runner_defect`,具体子类为“源目录身份断言误用于临时副本”。 - Attempt 1 finding 是“发布后尚未形成同源恢复点”;Attempt 2 finding 是演练器缺陷。两者均未证明备份不可恢复,不能合并表述为“备份连续失败两次”。 - Attempt 2 的正式批次前置门禁 `PASS`、临时复制完成、恢复前失败关闭、正式归档零修改和所有临时对象零残留,证明安全门禁在错误发生后按设计收口。 - 临时副本修订不是简单放宽:副本 manifest `runId` 必须重绑到源侧已验证的 run id,并继续通过 manifest、文件集、构建、时间、字节数、SHA-256 及源/副本元数据相等校验。 - 新增强制负例:源目录 basename 与 manifest run id 不配对必须 `NO_GO`;副本 manifest run id 即使内部自洽,但与源侧已验证 run id 不同也必须 `NO_GO`。 ## 2026-08-25 runner 修复验证与最新批次探针 本节仅追加,不改写 Attempt 1、Attempt 2 或既有探针记录。 ### Runner 修复验证 | 回归 | 结果 | | --- | --- | | 正式源 basename 与 manifest run id 配对 | `PASS` | | 同内容改名副本重绑源侧 run id | `PASS` | | 副本内容篡改 | 预期失败,`PASS` | | 源 basename 与 manifest run id 错配 | 预期失败,`PASS` | | 副本 manifest run id 内部自洽但不同于源 run id | 预期失败,`PASS` | | 动态 migration 集合正/反例 | `PASS` | | 既有隔离备份/恢复合同回归 | `PASS` | 实现结果:正式源目录继续校验 basename/run id;临时副本 manifest run id 必须等于源侧已验证 run id,并继续验证 manifest、文件集、构建、时间、大小、SHA-256 及源/副本脱敏元数据完全一致。 ### 08-25 最新自然批次 | 检查项 | 结果 | | --- | --- | | 演练开始时公网 `/ready` | `PASS` | | 公网构建与自动备份 unit | `MATCH`,短标识 `7b86ffb3`,timer `active` | | ARM 最新批次构建短标识 | `7b86ffb3` | | 来源与当前构建关系 | `SAME` | | 批次年龄 | `22249` 秒,约 6 小时 11 分,`within_24h=yes` | | run id 与目录配对 | `PASS` | | DB/附件 manifest 大小与 SHA-256 | `PASS` | | source image digest | 存在 | | manifest 摘要 | 已取得 12 位脱敏前缀,不保存完整摘要 | | 来源/当前 migration 目录 | 均为 65 个,差异文件 `0` | | 探针 root 入口及普通用户暂存 | 残留均为 `0` | | ARM 两个监控 timer | `active` | 该批次不是 Attempt 2 的旧候选,新鲜度和全部身份/完整性门禁已重新计算。当前满足 Attempt 3 恢复审批前提,但本次批准不包含实际恢复,未复制、解包或恢复该批次。 ## Attempt 3:数据库恢复阶段失败关闭 日期:2026-08-25 本节仅追加,不改写 Attempt 1、Attempt 2 或既有探针记录。 | 检查项 | 结果 | | --- | --- | | 候选文件 SHA-256、Bash 与 sudoers 语法 | `PASS` | | 最新正式批次前置门禁 | `PASS`;使用 08-25 新候选 | | 正式归档读取/修改 | 只读取用;runner 没有归档写入路径 | | 临时副本创建及源侧身份重绑 | `PASS`;否则不会进入数据库恢复阶段 | | 失败阶段 | `restore_database` | | runner 聚合类别 | `database_restore_failed` | | migration/12 类表/附件恢复与关联验证 | 未执行 | | runtime 清理 | `PASS`,复核目录残留 `0` | | 一次性 PostgreSQL 容器清理 | `PASS`,复核残留 `0` | | 临时 root 入口与库清理 | `PASS`,复核残留 `0` | | ARM 两个监控 timer | `active` | | 腾讯云生产公网 `/ready`(演练后) | `PASS`,构建短标识仍为 `7b86ffb3` | | 结论 | `NO_GO` | Attempt 3 在数据库恢复命令返回非零后正确停止,未进入 migration、关键表、附件恢复或附件关联检查。当前输出不能区分 dump 列表校验失败、恢复命令失败、工具/参数合同不匹配或其他 runner 调用问题,因此该次 `NO_GO` 不能被改写为“备份不可恢复”,也不能据此给出数据恢复 RTO 或完整恢复结论。 本轮还暴露了独立的演练器可观测性缺陷:`restore-db.sh` 的详细输出仅写入权限 `0600` 的本轮私有日志,而失败清理随后删除该日志,最终只保留 `database_restore_failed` 聚合类别。安全清理是正确的,但失败关闭前必须先生成不含路径、凭据、业务内容或原始 SQL 的阶段化脱敏诊断;在该合同补齐并通过回归、重新审批前,不执行 Attempt 4,也不改选旧批次或放宽门禁。 措辞对齐说明:既有 Attempt 2 段落中的“临时副本只跳过目录 basename 断言”仅描述不把自建目录名当归档证据,不构成放宽。生效合同始终是副本 manifest run id 必须等于源侧已验证 run id,并且副本全部脱敏元数据与源侧首次校验结果完全一致。 ## 2026-08-25 Attempt 3 后本地诊断修复 本节只追加本地 runner 与合成回归结果;未连接 ARM、未探测新批次、未执行 Attempt 4,也未修改腾讯云生产或 ARM 正式归档。 - runner 将数据库恢复拆为 `dump_list`、`target_confirmation`、`restore_apply` 三个阶段,只输出 `PASS/FAIL/NOT_RUN`、执行路径枚举、数字退出码、数字或 `UNKNOWN` 错误计数和稳定错误类别。 - allowlist 诊断在 trap/finally 等价清理之前输出;原始 stderr 仍只存在于权限 `0600` 的本轮私有日志并随 runtime 删除。注入退出码 `23` 的回归证明:阶段诊断先输出,runtime/access 清理随后均为 `PASS`,调用方在清理后仍能读取数字退出码。 - PostgreSQL 16 custom dump 直接驱动仓库 `scripts/restore-db.sh` 的 Docker stdin 路径成功;host-file 路径通过 PostgreSQL 16 工具执行同一真实脚本成功。没有在测试中重写等价恢复逻辑。 - dump-list 失败、目标确认失败和 list/target 通过后 apply 失败三条负例均通过;后续阶段保持 `NOT_RUN`,apply 非零不会因对象错误计数较小而降级为成功。 - 捕获诊断不含 fixture 凭据、连接串、SQL、临时路径或自由文本工具错误;既有合成成对备份/恢复回归通过,自动异机备份合同 `74` 项通过。 截至 Attempt 3,最新备份是否真的可恢复这个核心问题仍然没有答案。目前验证到的是演练器、恢复点身份门禁和每次失败后的安全收口,不是最新备份的数据可恢复性。Attempt 1 是发布后恢复点缺口;Attempt 2 是临时副本身份规则误用;Attempt 3 目前只能确定演练器可观测性不足,底层数据库恢复失败原因仍未知。三次均为 `NO_GO`,但不得把次数解释为“最新备份连续三次不可恢复”。 ## 2026-08-25 Attempt 4 前最新批次只读探针 本节仅追加探针与本地 Git 静态核对结果;未复制、解包或恢复批次,未安装恢复 runner,未执行 Attempt 4。 | 检查项 | 结果 | | --- | --- | | 演练前公网 `/ready` | `PASS`,构建短标识 `7b86ffb3c9ef` | | ARM 最新批次探针 | `PASS` | | 最新批次构建短标识 | `7b86ffb3` | | 来源提交唯一解析 | `PASS` | | 来源与当前生产构建关系 | `SAME` | | 批次年龄 | `26652` 秒,约 7 小时 24 分,`within_24h=yes` | | run id 与目录配对 | `PASS` | | DB/附件大小与 SHA-256 | `PASS` | | source image digest | 存在 | | manifest 脱敏摘要前缀 | `ee1f97a5b42d` | | 来源/当前 migration 集合 | `65/65`,差异条目 `0` | | 仓库 HEAD 相对生产构建的应用代码漂移 | 文件数 `0` | | 探针 root 入口清理 | `PASS`,残留 `0` | | 普通用户探针暂存 | 残留 `0` | | ARM 两个监控 timer | `active` | 该批次满足 Attempt 4 的构建关系、新鲜度、run id 配对、产物完整性和 schema 集合前置门禁;该结论仍不是恢复授权,也不回答最新备份是否可恢复。 恢复 runner 的持久化限制不予接受:截至本节时,修复后的 runner 与八类本地回归仍位于不提交的个人 workspace,仓库尚无可由其他执行者独立复跑的 runner 合同门禁。Attempt 4 实际恢复前必须先把 runner 的稳定来源和对应合同回归纳入 `scripts/qa/`,由合同脚本机械验证候选 SHA-256、三阶段诊断、源/副本身份、真实 `restore-db.sh` 双路径和失败清理;未完成前不安装 Attempt 4 恢复入口。 ## 2026-08-25 runner 持久化合同结果 本节仅追加本地跟踪文件与合同回归结果;未连接 ARM、未渲染现场候选、未执行 Attempt 4。 新增跟踪文件: - `scripts/qa/offhost-shadow-restore-runner.template.sh` - `scripts/qa/validate-offhost-shadow-restore-attachments.py` - `scripts/qa/run-offhost-shadow-restore-runner-contract.sh` | 合同类别 | 结果 | | --- | --- | | 私有参数渲染、确定性候选 SHA-256、shell 注入拒绝 | `PASS` | | 正式源身份与改名私有副本源侧重绑正例 | `PASS` | | 源 basename/run id 错配、副本篡改、副本 run id 与源不同负例 | `PASS` | | dynamic migration 集合指纹正/反例,无硬编码数量 | `PASS` | | 附件本地路径/大小/SHA-256 正例,路径逃逸与 S3 负例 | `PASS` | | 三阶段诊断、执行路径、两个退出码、后续 `NOT_RUN` | `PASS` | | 真实 `scripts/restore-db.sh` Docker stdin 与 PostgreSQL 16 host-file 路径 | `PASS/PASS` | | apply 非零失败关闭、诊断先于清理、清理后可读、无敏感自由文本 | `PASS` | 持久化模板不含远端地址、个人用户目录、远端账号、现场路径、实际容器/数据库名、生产构建值或 migration 指纹;这些值只能来自私有 JSON,并在候选写出前通过精确字段集、类型、字符集、绝对路径、shadow 命名和摘要格式校验。`check-sensitive-content.mjs` 对三个新增文件定向扫描结果为 `PASS`,扫描文件 `3`、finding `0`;本地测试容器残留 `0`。 Attempt 4 的 runner 产生过程已从个人 workspace 人工产物改为“跟踪模板 + 跟踪合同 + 私有参数确定性渲染 + 候选 SHA-256”。合同未通过不得生成或上传候选。该结果只补齐演练器可重复性,仍不构成最新备份可恢复性结论或 Attempt 4 恢复授权。 ## 2026-08-25 私有渲染与阶段日志门禁补强 本节仅追加本地门禁修订与合成回归结果;未连接 ARM、未创建现场私有配置、未渲染或上传 Attempt 4 候选,也未执行实际恢复。 - `.gitignore` 已增加专用 `/.restore-drill-private/` 目录;合同分别注入仓库内未忽略的私有 JSON 和未忽略的候选输出路径,两者均在候选创建前按预期失败。ignored 目录内的合成输入/输出正例通过,测试文件在退出时清理。 - 私有原始日志仍为 `0600` 且只在初始化时截断一次;PostgreSQL 容器启动、dump list、目标确认、restore apply 和附件恢复使用固定阶段标记追加。合同静态拒绝后续截断,最终 runtime 清理与只输出 allowlist 诊断的边界不变。 - PostgreSQL 16 fixture 增加连续成功查询的稳定就绪门禁,避免镜像首次初始化临时服务关闭窗口造成合成回归假失败;真实 `scripts/restore-db.sh` 字节未修改,Docker stdin 和 host-file 两条路径仍直接执行仓库脚本并通过。 - 本轮新增门禁回归全部 `PASS`;完成完整质量门禁并形成可追溯 Git 提交前,仍不得准备或上传 Attempt 4 候选。 完整本地门禁随后通过:runner 合同全部 `PASS`,既有隔离备份/恢复合成演练 `PASS`,自动异机备份合同 `74/74 PASS`;task validate、Trellis governance、process baseline、默认敏感扫描和三个新增文件定向敏感扫描均 `PASS`。文档审计错误 `0`,仅保留既有未登记文件 warning;`shellcheck` 在本机不可用,已以 Bash 语法、真实双路径集成回归和仓库合同覆盖。测试容器、本地私有渲染目录及候选残留均为 `0`。 ## Attempt 4 环境差异预先裁决规则 本节在 Attempt 4 新批次探针和实际恢复之前追加,不能按执行结果事后改写。 - Attempt 2 的源/副本身份重绑和 Attempt 3 的三阶段诊断已经由提交 `5ec7db1` 下的可复跑合同机械把关;合同失败时不得生成或上传候选。 - 若同一提交的合同、候选 SHA-256 和 ARM 安装前摘要均通过,但 Attempt 4 再次在恢复完成前暴露演练器侧失败,则默认 finding 升级为“本地合同环境与 ARM 实际执行环境存在系统性差异”,不再直接归为又一个孤立 runner 缺陷。 - 后续调查必须先形成脱敏环境差异矩阵,至少比较 Docker/Podman 实现与版本、PostgreSQL 16 镜像摘要和工具版本、shell/coreutils/openssl/python 行为、文件系统与权限/SELinux/AppArmor、可用 PATH、恢复脚本及依赖摘要、网络模式和资源约束。未取得差异证据前,不继续逐点放宽或修补 runner。 - 该规则不把任何失败预判为 ARM 环境问题,也不改变备份内容判定。若阶段诊断明确证明 dump、数据库对象、migration 或附件内容不满足既有合同,仍按对应数据恢复 finding 如实分类。 ## 2026-08-25 Attempt 4 执行前最终探针与候选 本节仅追加最新批次只读探针、候选本地渲染与合同结果;未上传或安装恢复候选,未执行 Attempt 4 实际恢复,未写腾讯云生产或 ARM 正式归档。 | 检查项 | 结果 | | --- | --- | | ARM 最新正式批次探针 | `PASS`;动态重选后仍为同一最新批次 | | 最新批次构建短标识 | `7b86ffb3` | | 来源与当前生产构建关系 | `SAME` | | 批次年龄 | `38572` 秒,约 10 小时 43 分,`within_24h=yes` | | run id 与目录配对 | `PASS` | | DB/附件大小与 SHA-256 | `PASS` | | source image digest | 存在 | | manifest 脱敏摘要前缀 | `ee1f97a5b42d` | | 探针临时 root 入口/sudoers 残留 | `0/0` | | 普通用户探针暂存 | 再次通过 SHA 清单后逐一删除,残留 `0` | | ARM dead-man/健康 timer | `active/active` | | 来源/当前 migration 目录 | `65/65`,差异条目 `0` | 候选由提交 `5ec7db1` 的 `scripts/qa/offhost-shadow-restore-runner.template.sh` 和 ignored 私有 JSON 确定性渲染。私有 JSON 权限为 `0600`、精确 20 个字段,runner 权限为 `0700`,输入与输出均命中仓库根锚定 ignore。两次独立渲染的字节和 SHA-256 完全一致;候选 SHA-256 为 `81f4eca2cf173196e1a57b781d28629412de7b7fe1a09b001b02d2025950391c`,合同报告值与本地 `sha256sum` 重算一致。 完整 runner 合同再次全部 `PASS`:源/副本身份、三条身份负例、dynamic migration、附件安全、五阶段追加日志、失败先诊断后清理、真实 `restore-db.sh` Docker stdin/host-file 双路径、退出码和敏感输出边界均通过。私有候选包共 6 个文件(runner、附件校验器、两份现有恢复脚本、sudoers、SHA 清单),包内 SHA 清单复核 `PASS`;runner 钉住的三份依赖摘要全部匹配,sudoers 固定命令与 runner 自身路径一致,`scripts/restore-db.sh` 相对生产构建仍无字节漂移,本地测试容器残留 `0`。 准备过程有一次安全失败:首次来源 migration 枚举误把 `migration_lock.toml` 当作 migration 名称,现场显示 `66/65`。该错误发生在候选渲染前,没有产生候选;随后改为只读取来源提交 `server/prisma/migrations` 树下的目录对象,并与当前工作树目录集合独立比较,结果为 `65/65`、差异 `0` 后才生成私有配置。Attempt 4 若获批准,安装前仍须在 ARM 复核上述候选包 SHA 清单;批次年龄和最新批次身份也须以实际执行开始时为准,超过 24 小时或最新批次变化时重新探针,不沿用本节结论。 ## Attempt 4:目标数据库确认阶段失败关闭 日期:2026-08-25 本节仅追加 Attempt 4 原始脱敏结果,不改写前置探针、候选合同或环境差异预先裁决规则。 | 检查项 | 结果 | | --- | --- | | ARM 安装前候选包 SHA 清单 | `PASS` | | 正式批次与私有副本前置门禁 | 已通过,否则不会进入 `restore_database` | | dump list | `PASS`,退出码 `0` | | target confirmation | `FAIL` | | 失败阶段/类别 | `restore_database` / `restore_target_confirmation_failed` | | 执行路径 | `DOCKER_STDIN` | | restore apply | `NOT_RUN` | | restore 脚本/真实 pg_restore 退出码 | `NOT_RUN/NOT_RUN` | | pg_restore 错误计数 | `NOT_RUN` | | 稳定错误类别 | `TARGET_CONFIRMATION_FAILED` | | migration/12 类关键表 | `NOT_RUN/NOT_RUN` | | 附件恢复与业务关联 | `NOT_RUN` | | runtime/临时 root 入口清理 | `PASS/PASS` | | 普通用户候选暂存 | `PASS`,残留 `0` | | 结论 | `NO_GO` | `dump_list=PASS` 只证明最新 custom dump 可由 ARM 的 PostgreSQL 16 工具读取目录;`target_confirmation=FAIL` 发生在实际恢复调用之前,因此本次没有数据库数据写入、migration 查询、关键表读取、附件解包或附件关联校验。数据恢复 RTO 未形成,实际执行时的精确 RPO 秒数未被失败输出保留;只能确认 runner 已在创建 runtime 前通过不超过 24 小时的硬门禁,不能用前置探针年龄冒充执行时精确 RPO。 本轮输出没有分别保留 target query 的退出码和“查询成功但数据库名不等”布尔值,现有证据尚不能区分连接/认证/工具调用失败与目标名称比较失败。正式归档在 runner 中只有读取和复制路径,未观察到归档写入;但失败发生在末尾源批次复校验之前,因此“正式归档零修改”仍需后续只读摘要复核,不以代码静态结论替代现场证据。演练后生产 `/ready` 也须另行补充,不能从 ARM timer 或清理 PASS 推断。 按 Attempt 4 开始前已提交的裁决规则,本次先登记为 `local_contract_vs_arm_environment_difference_investigation`,不是新的孤立 runner 缺陷,也不是备份不可恢复证据。下一步仅允许建立脱敏环境差异矩阵和补齐生产/归档只读复核;在差异证据、修订设计和新批准之前,不修改 runner、不创建 Attempt 5、不复用本批次盲重试。 ## Attempt 4 失败后的只读环境差异调查 本节仅追加失败后的只读 ARM 信息与本地合成复现;未在 ARM 启动调查容器、未读取业务数据、未修改 runner 或正式归档。 | 维度 | 本地合同环境 | ARM 实际环境 | 判定 | | --- | --- | --- | --- | | CPU/容器架构 | `x86_64` / `linux/amd64` | `aarch64` / `linux/arm64` | 不同 | | Docker client/server | `25.0.4/25.0.4` | `28.0.2/28.0.2` | 不同 | | PostgreSQL 16 镜像实例摘要前缀 | `fff3594bf464` | `c05eced0bdb4` | 多架构实例不同 | | 存储/cgroup/rootless | `overlay2/systemd/no` | `overlay2/systemd/no` | 相同 | | Bash/Python/OpenSSL/coreutils | `5.1.4/3.9.2/1.1.1w/8.32` | `5.1.4/3.9.2/1.1.1w/8.32` | 相同 | | AppArmor/SELinux | `Y/unavailable` | `Y/unavailable` | 相同 | | 工作文件系统 | `ext2/ext3` 类 | `ext2/ext3` 类 | 相同 | | 必需命令 | 合同执行通过 | `PASS` | 无缺失证据 | 差异矩阵本身不能证明 Docker 28 或 arm64 有缺陷。真正收敛根因的是启动时序覆盖不一致:tracked runner 在启动 PostgreSQL 后以第一次 `pg_isready` 成功结束等待;本地合同夹具曾真实出现“初始化临时服务正在关闭”的失败,之后夹具改成连续两次真实 `SELECT 1` 成功才继续,但 runner 的等待实现没有同步修改。于是合同环境稳定化后绕过了 runner 的竞态,而 ARM 仍执行首次 `pg_isready` 路径。 失败序列与该机制一致:dump list 不连接目标数据库,因此可在 PostgreSQL 初始化服务切换窗口中 `PASS/0`;紧随其后的 target query 需要真实连接,命中关闭/重启窗口即进入 `TARGET_CONFIRMATION_FAILED`,actual restore 尚未调用。本地使用相同镜像配置、相同随机凭据和 runner 中完全相同的 target query,在连续两次真实查询确认稳定后得到退出码 `0`、数据库名精确匹配、stderr 为空;本地测试容器随后清理为残留 `0`。 当前高置信度根因类别为 `postgres_initialization_readiness_race`,上位 finding 仍为开跑前定义的 `local_contract_vs_arm_environment_difference_investigation`。系统性差异具体表现为:合同夹具和 runner 没有复用同一稳定就绪路径,架构/容器版本造成的时序差异把该覆盖缺口暴露在 ARM。修复方向应是 runner 与合同共同执行“连续成功的真实目标数据库查询”并保留 target query 数字退出码与名称匹配布尔值,而不是为 ARM 增加一次固定 sleep、放宽目标确认或再次只修测试夹具。该修复和任何 Attempt 5 均须先更新设计、补可复跑负例并重新批准。 失败后只读残留复核:一次性容器、runtime、root 入口、恢复库目录、sudoers 和普通用户暂存均为 `0`;ARM dead-man 与公网健康 timer 均为 `active`。演练后生产 `/ready` 与正式归档摘要在此时仍待独立只读补证。 ## Attempt 4 后正式归档只读复核 日期:2026-08-25 本节追加 Attempt 4 后正式归档只读探针结果,不回写前置探针或 Attempt 4 原始失败记录。探针不复制、不解包、不恢复、不启动数据库容器,也不修改正式归档。 | 检查项 | 结果 | | --- | --- | | 只读探针 | `PASS`,退出码 `0` | | 最新批次构建短标识 | `7b86ffb3`,与当前生产构建相同 | | 探针时批次年龄 | `42007` 秒,约 11 小时 40 分,`within_24h=yes` | | run id 与目录配对 | `PASS` | | DB/附件产物完整性 | 大小与 SHA-256 聚合 `PASS` | | source image digest | 存在 | | manifest 脱敏摘要前缀 | `ee1f97a5b42d` | | 探针临时访问清理 | `PASS` | | 探针入口/暂存残留 | `0/0` | Attempt 4 前后探针的最新构建短标识、run id 配对、产物完整性和 manifest 脱敏摘要前缀一致,后置探针仍在 24 小时内通过。由此正式归档的内容级零修改已取得现场只读补证;该结论仅覆盖正式归档,不替代恢复成功证明,也不回答最新备份是否可恢复。 腾讯云生产演练后 `/ready` 仍未独立补证。ARM 的 timer active、清理 PASS 和归档探针均不能推导生产在线状态;在取得 `/ready` 正常状态及 12 位构建前缀前,AC2 保持未满足,证据不得表述为生产前后均正常。 ## Attempt 4 后生产只读补证与本地稳定就绪修复 日期:2026-08-25 执行方沿既有审计窄路径完成腾讯云生产演练后 `/ready` 只读核验:状态 `PASS`,12 位构建前缀 `7b86ffb3c9ef`,与演练前记录一致。地址和原始响应未写入本证据;该读取没有登录、写库、重启、切换或配置修改。至此 Attempt 4 的生产前后 `/ready` 均正常已补证,但不改变本轮 restore apply 未执行和最新备份可恢复性仍未知的结论。 用户随后批准的范围仅为本地 runner 修复与回归,不连接 ARM、不执行 Attempt 5。修复把 PostgreSQL 就绪从默认 Unix socket 的首次 `pg_isready` 改为容器内 TCP 回环 `127.0.0.1` 的真实 `SELECT current_database()`:initdb 临时服务器不监听 TCP,因此该判据结构性排除临时服务器;连续两次成功和最多 60 次有界重试只作为额外稳定保险。目标名称成功但不匹配立即失败,查询非零重置 streak;实际 `restore-db.sh` 仍走原路径且字节不改。 合同夹具与 runner 分叉审计结果: | 覆盖对象 | 合同机制 | 是否存在等价逻辑分叉 | | --- | --- | --- | | 私有配置渲染与候选 SHA | 直接使用跟踪模板和合同内唯一渲染器 | 否 | | 正式源/临时副本身份 | 从 rendered runner 提取 `validate_batch` 同一实现 | 否 | | PostgreSQL 目标就绪 | 从 rendered runner 提取同一 TCP 目标确认函数;真实 PostgreSQL 16 fixture 也调用该函数 | 原分叉已消除 | | DB restore Docker stdin/host-file | 直接执行仓库 `scripts/restore-db.sh` | 否 | | 附件引用安全 | 直接执行跟踪附件校验器 | 否 | | 失败诊断与 finally 清理 | 从 rendered runner 提取初始化和 finalization 同一实现 | 否 | | migration 指纹 | 对 rendered 值、目录集合变化和禁止硬编码做静态/合成断言 | 无等价重写;真实恢复集合仍待远端演练 | | 12 类表、storage 与完整编排 | 本地只做组件合同,不执行含真实备份的完整 runner | 明确覆盖限制,不能推断远端成功 | 就绪回归全部 `PASS`:socket 路径假定可用但 TCP 永久失败仍在第 60 次失败;首次查询失败后连续两次成功在第 3 次通过;一次成功后失败会清零 streak 并在第 4 次通过;目标名不匹配第 1 次立即失败;永久查询非零以 `TARGET_QUERY_NONZERO` 失败。真实 PostgreSQL 16 fixture 使用 extracted rendered-runner 函数就绪后,`restore-db.sh` Docker stdin 与 host-file 两条路径均成功;dump-list、目标错配、apply 非零、诊断先于清理、敏感输出负例继续通过。本地测试容器残留 `0`。 完整复跑结果:runner 合同全部 `PASS`,既有成对备份/恢复演练 `PASS`,自动异机备份合同 `74/74 PASS`;task validate、Trellis governance、process baseline、默认/定向敏感扫描和 `git diff --check` 均通过。文档审计错误 `0`,仅保留既有未登记文件 warning;`scripts/restore-db.sh` 相对生产构建无字节漂移,测试容器和生成缓存残留均为 `0`。 本地合同通过只证明演练器修复与组件路径,不回答最新正式备份是否可恢复。下一步仍须先提交可追溯变更,再动态选择最新正式批次、执行只读身份探针并重新渲染候选;Attempt 5 实际恢复必须另行批准。 ## Attempt 5 前最新正式批次只读身份探针 日期:2026-08-25 本节仅追加用户批准的最新正式批次只读探针与本地仓库关系核对;未执行恢复、未复制或解包正式归档、未启动 Docker、未读取数据库或附件内容。 | 检查项 | 结果 | | --- | --- | | 探针结果 | `PASS`;退出码未单独输出 | | 最新批次构建短标识 | `7b86ffb3` | | 来源与当前生产构建关系 | `SAME` | | 探针时批次年龄 | `64625` 秒,约 17 小时 57 分,`within_24h=yes` | | run id 与目录配对 | `PASS` | | DB/附件大小与 SHA-256 | `PASS` | | source image digest | 存在 | | manifest 脱敏摘要前缀 | `ee1f97a5b42d` | | 来源/当前 migration 目录 | `65/65`,差异 `0` | | 仓库 HEAD 相对生产构建的应用代码漂移 | 文件数 `0` | | 探针 root 入口/普通用户暂存残留 | `0/0` | | ARM dead-man/健康 timer | `active/active` | timer 状态只采用已验证私有配置中的实际 unit 名只读取得;一次使用猜测名称得到的无效读数已当场撤销,未进入证据结论。探针结果表明该最新批次继续满足构建、新鲜度、身份和完整性前置门禁,但仍不是恢复成功证明。 Attempt 5 最终候选尚未渲染。原因是用户提出的 RTO 分阶段记录已进入设计、但 runner 实现尚未另行批准;若先渲染,后续计时代码会改变候选字节和 SHA-256。必须先裁决并完成 RTO 八阶段本地合同,再从跟踪模板重新确定性渲染候选并核对 SHA-256,实际恢复仍须单独批准。 ## Attempt 5 前八阶段 RTO 本地合同 日期:2026-08-25 用户随后批准的范围仅为本地实现八阶段 RTO 计时并执行合同回归;本阶段未连接 ARM、未渲染现场候选、未生成 Attempt 5 候选 SHA-256,也未执行实际恢复。此前探针批次的 `64625` 秒新鲜度只属于探针时点,后续不得复用,实际恢复前必须重新选择最新正式批次并重跑只读身份门禁。 runner 现在以连续边界依次记录 `batch_copy`、`postgres_container_start`、`dump_list`、`target_confirmation`、`restore_apply`、`database_verify`、`storage_attachment_verify` 和 `final_integrity`。同一个时间点关闭前一段并开启后一段;成功路径八段均为非负整数且精确加总为唯一的 `data_restore_rto_seconds`。失败路径先结算当前阶段,保留已完成和当前阶段的数字,后续阶段为 `NOT_RUN`,然后才清理私有 runtime。最长 60 次 TCP 目标确认及其 1 秒间隔只位于 `target_confirmation` 边界内。 可复跑合同直接驱动 rendered runner 的同一计时函数。确定性成功夹具得到八段 `3/5/2/11/7/13/17/3` 秒,总计 `61` 秒且总字段只输出一次;目标确认失败夹具保留 `2/4/1/9` 秒、后四段 `NOT_RUN`、部分总计 `16` 秒,并证明计时输出位于 runtime cleanup 之前;restore apply 失败夹具记录当前段 `12` 秒、后续三段 `NOT_RUN`、部分总计 `28` 秒。捕获输出不含路径、连接串、凭据或 SQL。 完整本地门禁结果:runner 合同全部 `PASS`;真实 PostgreSQL 16 fixture 和未修改的 `scripts/restore-db.sh` Docker stdin/host-file 双路径继续通过;既有成对备份/恢复演练 `PASS`;自动异机备份合同 `74/74 PASS`;task validate、Trellis governance、process baseline、docs registry、敏感扫描和 `git diff --check` 通过。文档审计错误 `0`,保留既有未登记文件 warning;本地测试容器残留 `0`,`scripts/restore-db.sh` 相对生产构建仍无字节漂移。 Attempt 5 前增强停止线从本节起生效:八阶段计时完成并通过合同后,下一份满足 24 小时门禁的新鲜批次直接进入最终候选渲染、SHA-256 核对、只读探针和实际恢复审批。此后发现的改进项,除非会导致 Attempt 5 失败或产生错误结论,否则只登记为后续任务,不再作为 Attempt 5 前置增强插队。本地合同通过仍只证明演练器合同,不回答最新正式备份是否可恢复。 ## Attempt 5 最终候选与执行前只读探针 日期:2026-08-25 本阶段只执行用户批准的三件事:重新选择最新正式批次、从跟踪模板渲染最终候选并核对 SHA-256、运行一次自清理只读身份探针。未复制或解包正式归档、未启动恢复容器、未执行数据库或附件恢复,也未连接腾讯云生产。 | 检查项 | 结果 | | --- | --- | | runner 跟踪提交 | `0b94b97` | | 两次确定性渲染 | 字节一致,完整合同 `PASS` | | 最终候选 SHA-256 | `7904052aa5a8c31d54734bc2fbf2d9caf2dac8b0116135119519f8467b3dae27` | | 探针结果 | `PASS` / exit `0` | | 最新批次构建短标识 | `7b86ffb3`,与当前生产 `SAME` | | 探针时批次年龄 | `68889` 秒,约 19 小时 08 分,`within_24h=yes` | | run id 与目录配对 | `PASS` | | DB/附件大小与 SHA-256 | `PASS` | | source image digest | 存在 | | manifest 脱敏摘要前缀 | `ee1f97a5b42d` | | 来源/当前 migration | `65/65`,差异 `0`,候选指纹匹配 | | 应用代码/restore-db 字节漂移 | `0/0` | | 探针 access/entry/staging 残留 | `PASS/0/0` | | 本地合同容器残留 | `0` | 探针时距 24 小时门禁约剩 `17511` 秒,即约 4 小时 52 分。该余量不被静态批准延长;Attempt 5 runner 在实际开始时仍会动态选择最新正式批次并重新校验构建、run id、产物 SHA-256 和年龄。若执行被推迟到候选超龄且尚无更新批次,runner 必须在复制前失败关闭,不能沿用本节探针结论。 候选钉住的是 `EXPECTED_BUILD_PREFIX` 和 `EXPECTED_MIGRATION_SET_SHA`,不是本次探针批次的 source run id。runner 会在 ARM 上于实际执行时重新选择当时最新正式批次,再对实际批次执行构建前缀、目录/run id、三文件集合、大小、SHA-256、RPO 和恢复后 migration 集合门禁。如果探针后有更新批次原子落地,实际恢复输入可以合法切换到更新批次;最终证据必须记录实际批次的脱敏身份,并明确它与本次探针批次是否一致,不能仅因候选 SHA-256 不变就默认恢复了探针批次。 至此 Attempt 5 的前置批次身份、演练器合同、八阶段 RTO 和失败诊断均已具备;最新正式备份是否实际可恢复仍未得到答案,必须由另行批准的 Attempt 5 隔离恢复给出结论。 ## Attempt 5 实际结果与安装器路径根因 日期:2026-08-25 用户明确批准 Attempt 5 后,执行方在 ARM 安装前再次核对候选 SHA-256,完整值与已批准候选一致。紧邻执行前的只读探针在演练调用同一秒观测到最新正式批次年龄 `70444` 秒,来源构建与当前生产相同,run id/目录配对、DB/附件产物完整性、source digest 和 24 小时门禁均为 `PASS`。探针反推的批次创建时刻已作为操作侧脱敏时间证据保留。 runner 随后在 `entry_contract` 阶段以 `restore_library_missing` 失败关闭:`dump_list`、目标确认和 restore apply 均为 `NOT_RUN`,八个 RTO 阶段与总计也全部为 `NOT_RUN`,最终结果原样保留为 `NO_GO`。失败发生在 runner 选择实际恢复批次之前,因此本轮不存在可对外记录的“实际恢复批次”,也不能用紧邻探针批次代替实际恢复输入。正式归档复制、PostgreSQL 容器启动、数据库恢复、migration/12 类表核对、附件恢复与关联验证均未开始;这次结果没有证明最新备份可恢复,也没有证明备份不可恢复。 本次 finding 分类为 `installer_path_mismatch`,属于演练安装器缺陷。已批准 runner 的依赖合同指向固定 `libexec` 目录,本轮一次性安装器却把三个依赖安装到名称相似但不同的 `lib` 目录。runner 因而在正确位置找不到依赖并按入口合同失败关闭;其 `temporary_access_cleanup=PASS` 清理的是合同内目标,而错误目录未被该清理函数命中,这解释了初始 `attempt5_entry_residue=1` 与 runner 自报清理通过并存的原因。 对错误目录的定向只读诊断得到:`restore-db.sh`、`restore-storage.sh` 和附件校验器均存在、均为 root 拥有的普通文件,三者 SHA-256 脱敏前缀与本地安装包一致,额外条目数为 `0`。在目标身份和文件集合全部明确后,仅对该错误安装目录执行精确清理,结果 `runner_lib_cleanup=PASS`;该动作未使用宽泛目录匹配,也未触碰正式归档。 诊断脚本的自删除另外暴露出一个安装器层问题:脚本在 sudo 上下文中以 `$HOME` 推断普通用户暂存路径,实际解析为 root 家目录,因目标不存在而报 `unlink` 失败。这不影响错误依赖目录已清理的结论;随后由非 root 外层对普通用户暂存中的已知诊断脚本做精确删除。后续安装器不得在 sudo 脚本中使用 `$HOME` 推断调用用户路径,应由安装器预先固定并验证普通用户暂存目标,或由非 root 外层负责自清理。 演练后腾讯云生产 `/ready` 由既有审计窄路径只读取得 `PASS`,12 位构建前缀仍为 `7b86ffb3c9ef`,与演练前一致。Attempt 5 未进入正式归档选择和复制路径,也未启动容器或恢复写入,因此腾讯云生产与正式归档未被本次失败修改。 在任何下一次实际恢复前仍需做一次最终只读零残留复核,范围为:runner 入口、合同内 `libexec`、错误 `lib`、runner/probe sudoers 及 candidate、probe 入口、普通用户安装器/候选/诊断暂存、Docker 容器和 runtime,并确认 dead-man 与健康 timer 仍为 `active/active`。在该复核完成前,不把 Attempt 5 表述为“全部临时对象零残留”。 最小修订方案只涉及 ignored/workspace 安装器:将三项依赖的安装目标从错误 `lib` 对齐到 runner 已批准的 `libexec`,并修正普通用户暂存脚本的清理所有权;不修改已批准 runner,不重新渲染候选,候选 SHA-256 可保持不变。该安装器修订、本地校验、最新批次只读探针和任何再次实际恢复均需按既有批准边界分别推进。核心问题——最新正式备份能否完成数据库与附件隔离恢复——在 Attempt 5 后仍无答案。 ## Attempt 5 最终零残留复核与安装器最小修订 日期:2026-08-25 用户批准后,执行方沿既有 ARM 窄路径运行固定目标的最终复核入口。该入口只读取本轮已知入口、依赖目录、sudoers、暂存、容器/runtime 和两个监控 timer 的聚合状态,不读取正式归档或业务数据,不执行恢复。结果如下: | 检查项 | 结果 | | --- | --- | | runner/probe 入口、sudoers 与 candidate 残留 | `0` | | runner 合同内 `libexec` 残留 | `0` | | 安装器误用的 `lib` 残留 | `0` | | 普通用户安装器/候选/诊断暂存残留 | `0` | | 本轮容器残留 | `0` | | 本轮 runtime 残留 | `0` | | dead-man/健康 timer | `active/active` | | 最终复核 | `PASS` | | 复核脚本自身清理 | `PASS` | 至此 Attempt 5 初始 `entry_residue=1` 已由定向诊断、精确清理和独立最终复核闭环为全部已知临时对象残留 `0`。该清理结论不改变 Attempt 5 的 `NO_GO`,也不补造任何未运行的 RTO、数据库或附件恢复证据。 已批准的本地最小修订只改变 ignored/workspace 安装器,不进入 Git 历史:三项依赖安装目标由错误 `lib` 对齐到已批准 runner 中同一 `libexec`,静态检查确认安装器与 rendered runner 的目录类别一致;诊断脚本以自身调用路径作为自清理目标,并在删除前验证固定 basename、普通文件且非链接,不再读取 sudo `$HOME`。三个私有脚本通过 Bash 语法检查和 Git ignore 守卫;已批准 runner、附件校验器、恢复脚本和候选字节均未修改,候选 SHA-256 不变。 本地完整复核结果:影子恢复 runner 合同全部 `PASS`,真实 PostgreSQL 16 fixture 的 Docker stdin/host-file 路径继续通过,合成成对备份/恢复演练 `PASS`,自动异机备份合同 `74/74 PASS`;task validate、Trellis governance、process baseline、敏感扫描和 `git diff --check` 通过。文档审计错误 `0`,只保留既有未登记文件 warning;`scripts/restore-db.sh` 相对生产构建字节漂移 `0`,本地测试容器残留 `0`。 任何再次实际恢复仍须重新选择满足 24 小时门禁的新鲜批次、重新执行只读身份探针并取得独立批准。本节只闭环 Attempt 5 的环境清理和安装器路径缺陷,不回答最新正式备份是否可恢复。 ## 08-27 最新正式批次只读身份探针 日期:2026-08-27 本节只追加用户批准的最新正式批次只读探针。未复制、解包或恢复正式归档,未启动 Docker,未修改腾讯云生产、ARM 正式归档、timer 或企业微信。 探针准备前完成了安装器共享路径静态核对:runner 的 `SELF_PATH`、`LIB_DIR`、`SUDOERS_PATH` 与安装器目标精确一致,probe 的 `SELF_PATH`、`SUDOERS_PATH` 与探针安装器精确一致;`WORK_ROOT` 只由 runner 持有,安装器没有第二份编码;sudoers candidate 由最终 sudoers 路径推导。上述比较均位于首次 `sudo install` 之前,失败即不产生特权写入。 第一次上传准备因本地编排器错误截取 basename,三个文件以截短名称进入普通用户暂存;远端 SHA-256 门禁立即失败。该失败发生在 sudo 和探针执行之前,未安装 root 入口、未读取归档。确认暂存目录恰含三个预期的截短普通文件后,逐个精确删除并移除本轮暂存和安装器,再使用真实 basename 重新上传;重传包的两项 SHA-256、Bash 语法和三文件集合均通过。该 finding 属于 `local_probe_upload_orchestration_defect`,未影响 probe/runner 字节或 ARM 正式状态。 现场探针结果: | 检查项 | 结果 | | --- | --- | | 探针执行 | `PASS` / exit `0` | | 最新批次构建短标识 | `7b86ffb3`,与当前生产相同 | | 最新批次年龄 | `124137` 秒,约 34 小时 29 分 | | 24 小时门禁 | `FAIL`;超出 `37737` 秒,约 10 小时 29 分 | | run id 与目录配对 | `PASS` | | DB/附件大小与 SHA-256 | `PASS` | | source image digest | 存在 | | manifest 脱敏摘要前缀 | `ede8a77d9912` | | 探针合同内临时访问清理 | `PASS` | | 探针入口/普通用户暂存残留 | `0/0` | `probe_result=PASS` 只表示只读探针完整执行并成功校验所见批次,不等于恢复前置门禁通过。按 PRD 的 24 小时硬门禁,本次结论为 `NO_GO/stale_latest_batch`:不得执行实际恢复,也不得改选更旧批次掩盖最新恢复点不新鲜。构建、身份和产物完整性通过说明当前证据不指向归档损坏;但仅凭本次探针不能区分源端自动备份未产出、传输/finalize 失败或调度时点变化。 下一步须另行批准只读调查生产自动备份最近运行状态、ARM receiver 最新原子归档时刻以及 dead-man/健康 timer 状态。该调查不得手工触发备份或修改 timer。只有新的正式批次满足 24 小时门禁并重新通过身份探针后,才能再次申请实际隔离恢复批准;既有恢复授权不延续。 ## 08-27 自动备份链与 dead-man 只读调查 日期:2026-08-27 用户批准的调查只读取生产自动备份 systemd 聚合状态、ARM receiver 原子归档/inbox 聚合状态、dead-man 状态和两端文件系统容量。未触发备份、未修改 timer、未执行恢复、未改正式归档、未发送测试告警,也未输出远端身份、现场路径、原始 JSON、journal、凭据或完整 run id。 生产侧结果:daily timer 与 watchdog timer 均为 `active`,最近分别在当日计划时点触发;对应 service 最近一次均为 `failed/result=exit-code/exit=1`。因此“timer 未调度”被排除,失败位于两项服务共用的备份运行合同或其后续路径。批准的部署账号不具备 sudo,也不能穿越 root-owned 状态/本地备份目录;调查明确记录 `ACCESS_DENIED`,没有把无权读取改写成状态文件或本地批次不存在。该权限边界使生产侧原始失败 phase 保持未知,不据此猜测具体 stderr。 ARM 侧结果:dead-man timer 与公网健康 timer 均为 `active`;dead-man service 最近一次按失败状态退出。实际 dead-man 阈值为显式参数 `93600` 秒(26 小时)。`last-deadman` 为 `FAIL_OFFHOST_DEADMAN/archive_stale/findingCount=1`,alert state 同为 `FAIL_OFFHOST_DEADMAN/archive_stale`。最近 72 小时 journal 仅做 allowlist 聚合,得到 `sent=true` 一次、`sent=false` 两次,最近一次为 `sent=true`。该次 dead-man timer 触发锚点为 2026-08-27 06:30:06(Asia/Shanghai),alert state 在同轮随即更新,可作为本次发送调用时刻。恢复演练探针在同日稍后首先促成人工响应,但当时未独立采集“人员实际知晓”的精确墙钟时刻,因此本轮不能伪造精确响应延迟,只能登记为“发送调用早于人工响应、精确差值未测”。后续事件任务必须同时记录 `sent=true` 与首次确认时刻,直接计算真实响应延迟。 检测侧结论为正面:真实超龄被正确检测,告警 helper 至少完成了一次发送调用,且相同失败的 `sent=false` 属于正常去抖;08-23 去抖改造没有造成检测或发送调用盲区。响应侧仍有 finding:本次备份中断最终由恢复演练探针触发处置,而不是由既有告警形成及时响应。证据不证明具体人员已阅读消息,后续应调查群消息送达、可见性、责任人确认和升级流程,不能把检测调用成功等同于闭环响应。 receiver 聚合结果:正式原子日归档共 7 个,最新归档在复核时年龄约 43 小时 40 分;inbox 有 2 个 `.partial`,正式归档事务临时对象为 0、inbox 其他对象为 0。两个 partial 均不满足“三个普通文件 + manifest/run 身份 + 产物字节数”的完整传输合同,完整合同数量为 0、不完整数量为 2;最新 partial 年龄约 19 小时 40 分,最旧 partial 约 31 天 10 小时。由此将当前事故分类为 `offhost_transfer_incomplete`:生产 daily 确实触发并在 ARM 留下未完成传输,不能归为源端完全未产出、timer 漏调度或“传输已经完整但 finalize 失败”。partial 的存在同时证明 rsync 已通过初始 SSH 认证、host key 校验和 receiver 授权并开始写入,因此密钥失效、authorized_keys 初始拒绝和 host key 初始拒绝不是本次首要候选;排查应收缩到传输中段的超时/网络中断、源端读取、目标端写入或会话/传输限制。最旧 partial 同时是历史清理/观测 finding,但本轮没有删除它。 当前只读字段还不足以在上述传输中段候选之间继续裁决。下一轮独立生产事件调查应比较两个 partial 的聚合字节大小/停止比例,读取 wrapper 私有诊断中的真实 rsync 退出码,并记录每轮传输持续时间:相近停止偏移或相近时长支持确定性限制,不同偏移/时长更支持网络或超时;service 的通用 exit `1` 不得替代 rsync 原始码。该调查仍只输出字节聚合、退出码和耗时,不输出文件名、路径或 stderr 原文。 容量必须与传输修复并行处理:生产侧相关路径位于同一可识别文件系统,使用率约 82%、可用约 10 GiB;ARM archive/inbox 文件系统使用率约 53%、可用约 89 GiB。现有证据仍不足以把这次 rsync 中断直接归因于磁盘不足,但自动备份会先在生产生成完整 dump 与 storage 归档,持续失败还会反复生成/保留本地产物;10 GiB 对该流程属于薄余量。若继续下降,故障可能从“传输中断”升级为“源端无法产出”,因此容量治理不是低优先级后续项,而是独立生产事件中的并行处置项。 调查器本身出现并安全关闭了两类本地缺陷:首次生产探针错误假设部署账号可 sudo,安装前即被权限拒绝;前两次 ARM 探针把 dead-man unit 写成近似但不存在的名称,均在状态读取前以合同不可读失败。每次 root 入口、sudoers candidate、普通用户暂存均为 0。最终版本从仓库 receiver 安装器机械提取并比较 unit basename,并将生产探针改为无提权只读执行;Bash/Python 语法、两种 ExecStart 解析 fixture 和敏感扫描通过。上述缺陷不改变生产、ARM、归档、timer 或告警状态,也没有伪造调查结果。 当前独立生产 finding 为:最新可用异机恢复点已超过 24 小时目标,自动备份传输链正在失败;恢复演练因此保持阻塞。该事件不再作为恢复演练的普通 finding 排队处理,建议另建 Trellis 生产事件任务承载传输/容量调查与修复,当前演练任务仅保留证据交叉引用和阻塞关系。后续优先顺序是另行批准修复生产到 ARM 的传输链并形成新的原子批次,确认自动备份/两端 dead-man 与 watchdog 恢复,再对新批次重新执行身份探针。手工补跑备份、修改网络/SSH/timer、清理 partial 或发送测试告警均不在本次只读批准内。 ## 2026-09-09 最小数据库隔离恢复通过(管理员执行回传) 证据来源:用户在 ARM 既有 root 交互会话执行本会话提供的分步命令,回传脱敏 JSON;代理未获得该会话的管理员凭据,未独立登录读取恢复库或核验远端清理。代理独立完成的是本地来源提交 migration 集合提取、原恢复脚本摘要核对及普通 SSH 临时上传。以下远端结果按管理员执行回执记录,不冒充独立复核。 - 批次创建时间:2026-09-08T18:30:05+00:00,即北京时间 2026-09-09 02:30:05;完整性回执年龄 7.35 小时,不是后续任意时点的当前年龄。 - manifest SHA-256:`968426af94092cd05a672817178e46773837d893fc9bd0491a4c76d3a49a611f`;来源构建 `f223bfb72fb2baef51199702c2def84c41af7aef`。 - 数据库包和配对附件压缩包均大小与 SHA-256 匹配。附件仅校验压缩制品,未解包、未做附件引用验证。 - 镜像为既存 `postgres:16-alpine`,linux/arm64;固定镜像 ID `sha256:c05eced0bdb41ea9b95a656472a6aa4d50cad0d8a2e33d14eb1c53fd6204f2ae`。先前只检查 `postgres:16` 的 false 不能解释为所需镜像缺失。本轮未下载镜像。 - 执行前回传 archive/docker 可用空间均 89.35 GiB;恢复命令另设磁盘及可用内存下限检查。此值不是生产容量或清理后容量。 - 使用原版 `restore-db.sh`,SHA-256 `3707d81db02364c5a4917f230ddde721731c04aa66ab746bd23a7bdcd475b95d`;一次性 PG16 容器无宿主端口、network=none、独立本轮卷。连续两次 TCP 回环确认影子库后执行,恢复脚本退出码 0,未自动重试。 - 验证只读事务使用 ON_ERROR_STOP;来源 migration 集合 66 项精确匹配、异常 migration 0。名称集合 SHA-256 为 `18967953c57e88cab87453074e7e2c5a566482b60da1f67276d342b976c99d35`。本地从来源 Git 提取,非旧证据中 65 项硬编码;与本地候选 `b918530` 的目录集合一致。未执行 migrate 或 seed。 | 关键表 | 恢复后行数 | | --- | ---: | | departments | 5 | | employees | 8 | | employee_roles | 14 | | purchase_requests | 0 | | reimbursement_forms | 6 | | invoice_pool | 36 | | payment_batches | 2 | | contract_registry | 0 | | projects | 0 | | purchase_attachments | 0 | | reimbursement_attachments | 5 | | payment_batch_receipt_attachments | 1 | 最终回执:`DB_RESTORE_PASS`,12 类关键表全部可读;`storage=NOT_TESTED`、`app=NOT_TESTED`。行数只描述该具体备份恢复后的聚合结果,不是与实时生产逐项比对或业务流程可用的证明。 清理回执:`cleanupPending=false`、`containerRemoved=true`、`volumeRemoved=true`、`temporaryFilesRemoved=true`。检查通过后精确删除本轮容器、卷、临时凭据、日志及上传脚本;命令不删除正式归档、不清理旧回滚镜像、不改权限/sudoers或生产服务。本轮未独立查询生产健康或 ARM 监控,不能据此勾选原整体演练的前后健康验收。 结论限于本批次:已实际证明数据库恢复命令及约定数据库检查通过,不能继续笼统记为“本任务当前正式备份数据库恢复能力仍无答案”。历史五次 NO_GO 保留,Attempt 3 原因仍未知;本次成功不追溯解释历史失败,也不证明所有备份都可恢复。配对附件恢复、应用恢复/业务验收、完整灾备 RTO/RPO 和独立复核均未完成,整体 task 不关闭、不归档,不自动取得发布例外或部署资格。