修复异机备份凭证与中断恢复(07-27-offhost-backup-final-fixes)
这个提交包含在:
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,75 @@
|
||||
# 异机备份最终收尾修复设计
|
||||
|
||||
## 边界与顺序
|
||||
|
||||
本任务只修三类已确认缺陷:凭证进入 argv、紧急恢复卡口径漂移、finalize 被强杀后的孤儿事务。receiver 的恢复入口固定为:校验配置 → 取得 `.finalize.lock` → 恢复或拒绝历史孤儿事务 → 校验本次批次 → 发布/取代 → 周检查点 → 保留轮转。没有锁时不得扫描或改变孤儿对象。
|
||||
|
||||
## 凭证传递合同
|
||||
|
||||
### 生产 wrapper
|
||||
|
||||
`capture_command` 仍负责将 stdout/stderr 写入 root-only 诊断,但不再把 `env` 当作被执行命令。调用方进入隔离子 shell,在子 shell 内导出 `DATABASE_URL`、Docker DB 身份、备份目录和构建标识,再调用:
|
||||
|
||||
```bash
|
||||
(
|
||||
export DATABASE_URL DB_TOOL_MODE DB_CONTAINER POSTGRES_DB POSTGRES_USER \
|
||||
POSTGRES_PASSWORD STORAGE_DIR_HOST BACKUP_DIR BACKUP_RUN_ID BUILD_SHA \
|
||||
SOURCE_IMAGE_DIGEST
|
||||
capture_command paired_backup "$ROOT_DIR/scripts/backup-all.sh"
|
||||
)
|
||||
```
|
||||
|
||||
敏感值仍只存在于执行进程环境和内存,不写入命令 argv、诊断、summary 或日志。本任务不宣称对 root 或同执行身份读取 `/proc/<pid>/environ` 提供额外隔离。
|
||||
|
||||
### Docker DB 工具
|
||||
|
||||
生产 Docker 已证明无值 `-e VAR` 能继承客户端环境。三处 Docker 调用统一为:
|
||||
|
||||
```bash
|
||||
PGPASSWORD="$POSTGRES_PASSWORD" docker exec [-i] -e PGPASSWORD \
|
||||
"$DB_CONTAINER" <pg-command-without-password>
|
||||
```
|
||||
|
||||
`PGPASSWORD` 的值不得出现在 Docker CLI 参数,mock 测试同时检查参数不含合成密码、容器命令能看到继承值。测试夹具用于启动命令的普通 `env KEY=value` 不在本轮机械改写范围。
|
||||
|
||||
## 日归档孤儿恢复
|
||||
|
||||
receiver 在锁内扫描 `.daily-replacement-YYYYMMDD.*`。每个孤儿必须是受控 archive 根的一层普通目录且非链接;从其 manifest 取得原 `runId`,执行与正式 finalize 相同的精确文件集、大小和 SHA-256 自校验,并验证日期与孤儿名一致。
|
||||
|
||||
| 现场 | 含义 | 正确动作 |
|
||||
| --- | --- | --- |
|
||||
| 孤儿存在且当日正式归档存在、两者均有效 | 新批次已换入,只缺最后清理 | 保留正式归档,删除孤儿 |
|
||||
| 孤儿存在且当日正式归档缺失、孤儿有效 | 旧归档已移开,新批次尚未换入 | 按孤儿 manifest 的 `runId` 原子恢复旧正式归档 |
|
||||
| 两者都在但当日正式归档校验不过 | 无法判断换入结果 | fail closed,两个都保留 |
|
||||
| 孤儿无效、同日多个正式归档、同日多个孤儿、名称/manifest 日期不符或任一对象为链接 | 状态歧义或受污染 | fail closed,所有对象保留 |
|
||||
|
||||
恢复旧归档或确认新归档后,才允许继续处理本次 partial。任何删除前再次验证目标仍是刚才检查过的严格路径和类型。
|
||||
|
||||
## 周检查点孤儿恢复
|
||||
|
||||
周检查点是日归档的硬链接派生物,但仍不得猜测或直接删除。每个 `week-YYYY-Www.new.*` / `.rollback.*` 先按正式周名分组,要求每类最多一个普通目录且非链接。正式周点和 rollback 必须自校验 manifest,且 manifest 日期属于该 ISO 周;`.new` 是从未提交的 staging,内容可能因强杀而不完整,不能作为恢复来源,也不要求内容有效,但只有在另一个有效权威来源已经确定后才可清理。
|
||||
|
||||
| 现场 | 含义 | 正确动作 |
|
||||
| --- | --- | --- |
|
||||
| rollback 存在、正式周点缺失、rollback 有效 | 正式周点已移开,新点尚未换入 | 原子恢复 rollback 为正式周点;`.new` 路径/类型安全时删除 staging,不使用其内容 |
|
||||
| rollback 存在、正式周点存在且正式点有效 | 新周点已换入,只缺清理 | 保留正式周点,删除 rollback;`.new` 路径/类型安全时删除 staging |
|
||||
| 仅 `.new` 存在、正式周点存在且有效 | staging 未提交或残留 | 保留正式周点;`.new` 路径/类型安全时删除 staging |
|
||||
| 仅 `.new` 存在、正式周点缺失 | 没有已提交周点 | 先找到该 ISO 周唯一有效的正式日归档来源;能唯一确定时删除 staging 并从该日归档重建,不能唯一确定时 fail closed 并保留 `.new` |
|
||||
| 正式周点无效、rollback 无效、多个候选、日期/周不符或任一对象为链接 | 状态歧义或受污染 | fail closed,全部保留 |
|
||||
|
||||
若日归档恢复动作影响某 ISO 周,必须从最终选定且已验证的正式日归档重建该周检查点后才算 reconciliation 成功。周点恢复/重建失败时不删除日归档 rollback。
|
||||
|
||||
## 失败传播与可观测性
|
||||
|
||||
reconciliation 任一歧义返回非零,receiver 不处理本次 partial。源端 SSH finalize 随即在 `offhost_finalize` 阶段失败,`capture_command` 保留 `0600` 诊断,`on_error` 通过独立企微群机器人发送脱敏类别,且不写 `last-success.json`、不清理源端批次。若故障持续,ARM dead-man 在超龄窗口独立告警。该双层链路已经真实验证,本任务只用合成测试证明返回值和源端状态传播,不再次制造真实断链。
|
||||
|
||||
## 文档与单管理员恢复卡
|
||||
|
||||
runbook 标题改为“紧急恢复卡(单管理员离线版)”,正文只保留唯一系统管理员、ARM 隔离影子恢复、真实生产恢复另行批准和不粘贴敏感内容的约束。复制到 ARM 与离线副本是后续可选运维动作,不写死真实路径、不进入代码验收。
|
||||
|
||||
## 兼容、部署与回滚
|
||||
|
||||
- 不改变批次、manifest、receiver forced-command、timer、保留数量或告警 payload 合同。
|
||||
- 本地提交后先停在外部门禁;推送、生产 runner/DB 脚本部署、ARM receiver 更新分别请求批准。
|
||||
- 代码回滚只回滚本任务提交;已存在的正式归档、周检查点、partial 和孤儿对象均不删除。
|
||||
- 完成后归档本任务并结束运维加固主线。
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,55 @@
|
||||
# 异机备份最终收尾修复实施计划
|
||||
|
||||
## 实施步骤
|
||||
|
||||
- [x] 先扩展合成测试:记录 mock Docker argv 与继承环境,证明四条凭证路径不含合成密码/完整连接串且容器能收到 `PGPASSWORD`。
|
||||
- [x] 修改 `run-automatic-backup.sh`,在隔离子 shell内导出成对备份变量后调用 `capture_command`,移除生产路径 `env KEY=value` argv。
|
||||
- [x] 修改 `backup-db.sh` 和 `restore-db.sh` 三处 Docker 调用,使用前缀环境赋值配合无值 `-e PGPASSWORD`;不批量改写测试夹具。
|
||||
- [x] 为 receiver 增加锁内 self-validation 与 orphan reconciliation helper,严格实现 `design.md` 的日归档和周检查点状态表。
|
||||
- [x] 增加孤儿测试矩阵:明确恢复、明确清理、周点重建、非法类型/链接/重复候选/校验失败全量保留,以及 fail-closed 到源端状态的合成传播。
|
||||
- [x] 更新 runbook 恢复卡标题和正文为单管理员口径;同步 `.trellis/spec/hyb-oa/deployment-trialrun.md` 的凭证 argv 与中断恢复稳定合同,不新建平行文档。
|
||||
- [x] 运行聚焦与全量相关门禁,完成只读差异复核;不连接真实告警、不写 ARM/生产、不执行 migration。
|
||||
- [x] 经用户确认后提交中文 commit;推送和生产/ARM 部署另行批准。
|
||||
- [ ] 部署验证完成后归档本任务,硬收口运维主线并返回产品 backlog;不得因本轮审查再新增运维加固 task。
|
||||
|
||||
## 预计文件边界
|
||||
|
||||
- `scripts/run-automatic-backup.sh`
|
||||
- `scripts/backup-db.sh`
|
||||
- `scripts/restore-db.sh`
|
||||
- `scripts/receive-offhost-backup.sh`
|
||||
- `scripts/qa/run-automatic-backup-contract.sh`
|
||||
- 必要时扩展现有 `scripts/qa/run-backup-restore-drill.sh` 的 argv 断言,不新增平行 runner。
|
||||
- `deploy/trialrun/runbook.md`
|
||||
- `.trellis/spec/hyb-oa/deployment-trialrun.md`
|
||||
- 当前 Trellis task 三份规划/证据文件。
|
||||
|
||||
## 验证命令
|
||||
|
||||
- `bash -n scripts/run-automatic-backup.sh scripts/backup-db.sh scripts/restore-db.sh scripts/receive-offhost-backup.sh scripts/qa/run-automatic-backup-contract.sh scripts/qa/run-backup-restore-drill.sh`
|
||||
- `bash scripts/qa/run-automatic-backup-contract.sh`
|
||||
- `bash scripts/qa/run-backup-restore-drill.sh`
|
||||
- `node deploy/trialrun/validate-env-contract.js`
|
||||
- `python3 ./.trellis/scripts/task.py validate .trellis/tasks/07-27-offhost-backup-final-fixes`
|
||||
- `node scripts/qa/check-trellis-governance.mjs`
|
||||
- `node scripts/qa/check-sensitive-content.mjs`
|
||||
- `node scripts/qa/audit-docs.mjs --check-registry`
|
||||
- `node scripts/qa/check-process-contract.mjs --baseline --json`
|
||||
- `git diff --check`
|
||||
|
||||
## 外部门禁与回滚点
|
||||
|
||||
- 生产 Docker 无值 `-e VAR` 继承只读实测已完成;实施阶段不重复读取生产配置或凭证。
|
||||
- 推送当前 3 个未推送提交与本任务提交,需要提交完成后另行批准。
|
||||
- 更新生产自动备份脚本、ARM receiver、手工触发真实备份或复制恢复卡到 ARM,均需分别明确批准。
|
||||
- 生产部署前只读确认活动构建、62 项 migration、四组 timer 和最近成功状态;本任务没有 migration。
|
||||
- 任一 orphan 合成负例发生对象减少、正式恢复点丢失或歧义状态被自动选择,立即停止,不进入部署。
|
||||
- 回滚只回退本任务脚本/文档,不清理现场备份、partial、rollback 或已归档 task。
|
||||
|
||||
## 本地实施与验证证据(2026-07-27)
|
||||
|
||||
- 凭证传递已覆盖 wrapper、nightly Docker 备份、Docker 影子确认和恢复;mock 捕获的 argv 不含合成连接串/口令,容器命令仍收到继承的 `PGPASSWORD`。
|
||||
- receiver 合同共 73 项通过。除原有同日取代、保留轮转、dead-man 和失败传播外,新增日/周孤儿恢复、重复/链接/损坏歧义 fail closed,以及“周 reconciliation 失败时日 rollback 不提前删除”的回归断言。
|
||||
- `run-backup-restore-drill.sh` 通过,隔离合成 RTO 为 9 秒;`validate-env-contract.js` 返回 `[R26][PASS]`。
|
||||
- shell 语法、task validate、Trellis governance、敏感内容扫描、文档注册审计、流程 baseline 和 `git diff --check` 均通过。文档审计仅报告仓库既有 22 个未注册 docs 文件警告,本任务未新增 docs 文件。
|
||||
- 只读复核发现并修复了“先删日 rollback、后重建周点”的顺序缺陷;稳定规则已同步到 `deployment-trialrun.md`。本地过程未连接真实企微、ARM 或生产,也未执行 migration。
|
||||
@@ -0,0 +1,62 @@
|
||||
# 异机备份最终收尾修复
|
||||
|
||||
## Goal
|
||||
|
||||
在不扩建运维平台的前提下,完成自动异机备份最后一轮有界修复:让生产热路径和 Docker 备份/恢复入口不再把数据库凭证放入进程 argv;让紧急恢复卡与唯一系统管理员的事实一致;让 ARM finalize 在被强杀后能够安全恢复明确的孤儿事务,并在歧义状态下保留全部现场、失败告警而非静默停摆。
|
||||
|
||||
## Background
|
||||
|
||||
- `origin/main` 与当前生产活动构建均为 `a0006f0`;`2526052` 已推送,生产已有 62 项成功 migration,手机号 nullable migration 不需要重复部署。
|
||||
- 当前待推送提交只有 `de35ab8`、`95b51e6`、`f1e8cf1`;本任务完成前不单独推送,避免本地与远端继续形成多个收尾批次。
|
||||
- `scripts/run-automatic-backup.sh:344` 使用 `env KEY=value` 调用成对备份,完整 `DATABASE_URL` 和数据库口令会进入 `env` 进程 argv。
|
||||
- `scripts/backup-db.sh:74`、`scripts/restore-db.sh:101`、`scripts/restore-db.sh:113` 使用 `docker exec -e PGPASSWORD=value`,其中 nightly 自动备份会经过 `backup-db.sh`,因此只修 wrapper 不能关闭热路径暴露。
|
||||
- 生产 Docker `20.10.24` 已用无敏感合成变量只读实测:`VAR=value docker exec -e VAR ...` 能把客户端环境值继承给容器命令;测试只记录 `inherited=yes`,未读取或输出数据库凭证。
|
||||
- `deploy/trialrun/runbook.md` 的紧急恢复卡仍写“复制到公司知识库”和“第二名内部人员”,与已经批准的唯一系统管理员口径矛盾。
|
||||
- receiver 已有 `flock` 和正常错误回滚,但 `SIGKILL` 无法执行 shell 清理;强杀窗口可能留下 `.daily-replacement-*`、`.new.*` 或 `.rollback.*`,下一次 finalize 当前没有启动恢复逻辑。
|
||||
|
||||
## Scope
|
||||
|
||||
- 修复 wrapper 与三处 Docker DB 工具调用的凭证 argv 暴露,并增加合成回归断言。
|
||||
- 增加 receiver 锁内日归档/周检查点孤儿事务恢复及 fail-closed 测试矩阵。
|
||||
- 同步单管理员紧急恢复卡和部署规约;完成本地验证、提交与后续受控部署后归档运维主线。
|
||||
|
||||
## Requirements
|
||||
|
||||
- 四处已确认的凭证调用必须改为环境继承,敏感值不得出现在调用命令 argv:wrapper 在隔离子 shell 中导出所需变量后调用 `capture_command`;Docker 入口使用 `PGPASSWORD="$POSTGRES_PASSWORD" docker exec -e PGPASSWORD ...`,不得使用 `env DATABASE_URL=...` 或 `-e PGPASSWORD=...`。
|
||||
- 契约测试中的 `env KEY=value` 是合成夹具,不含真实凭证,不做无意义的批量改写;新增断言必须使用合成标记证明敏感值不出现在 mock 捕获的 argv。
|
||||
- 紧急恢复卡标题、负责人和季度动作必须统一为单管理员口径:恢复卡保存在现有 runbook,由唯一系统管理员维护并按季度在 ARM 隔离目标执行影子恢复;不虚构知识库副本或第二人门禁。
|
||||
- finalize 必须在取得现有独占锁之后、处理本次 partial 之前,扫描严格命名的日归档和周检查点孤儿事务;正式点和 rollback 只有在普通目录、非符号链接、位于受控根目录且通过 manifest/文件集/大小/SHA-256 自校验后才可作为权威来源。`.new` 永远不是权威来源,只能在严格路径/类型校验且另一个有效权威来源已经确定后清理。
|
||||
- `.daily-replacement-*` 的判定必须遵守 `design.md` 的日归档状态表:孤儿与有效当日归档同时存在时删除孤儿;孤儿存在但当日归档缺失时从孤儿 manifest 还原原归档名;当日归档校验失败、存在多个候选或任何身份歧义时 fail closed,两个对象均保留。
|
||||
- 周检查点 `.new.*`/`.rollback.*` 必须遵守 `design.md` 的对称状态表:rollback 在正式周检查点缺失时优先恢复;正式周检查点有效时临时对象才可清理;只有 staging 且正式点缺失时,必须先找到同 ISO 周唯一、有效的日归档来源再重建;歧义或校验失败一律保留现场并失败。
|
||||
- 自动清理必须逐目标执行严格路径、类型和命名校验;不得用宽泛 glob 删除,不得触碰正式日归档、正式周检查点、手工批次或最后一个有效恢复点。
|
||||
- orphan 歧义导致 receiver 非零时,源端必须继续在 `offhost_finalize` 阶段失败、保留源批次和 `0600` 诊断并走独立群机器人告警;ARM dead-man 继续在超龄窗口独立告警。本任务复用已真实验证的链路,不再人为制造第二次真实断链。
|
||||
- 恢复卡复制到 ARM 和离线副本属于可选纯运维缓解项,不是代码验收阻断;任何 ARM 写入、推送、生产部署或真实备份执行仍需另行批准。
|
||||
- 本任务完成后硬收口运维加固,不继续新增备份平台、告警能力、保留层级或恢复自动化,后续返回产品 backlog。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] 合成 argv 测试证明 wrapper、nightly Docker 备份、Docker 影子确认和 Docker 影子恢复四条路径的命令参数均不包含合成密码或完整连接串,同时容器命令仍收到 `PGPASSWORD`。
|
||||
- [x] `run-automatic-backup.sh` 不再出现生产路径 `capture_command ... env DATABASE_URL=...`;`backup-db.sh`/`restore-db.sh` 不再出现 `docker exec ... -e PGPASSWORD=value`。
|
||||
- [x] 恢复卡正文、标题、负责人和季度动作与《管理员手册》单管理员口径一致,且仍保留真实生产恢复的单独批准门禁。
|
||||
- [x] 日归档测试覆盖“孤儿+有效正式点则清理孤儿”“孤儿+缺正式点则恢复旧点”“孤儿+无效正式点则两个都保留并失败”“重复/非法/链接候选则失败且不删除”。
|
||||
- [x] 周检查点测试覆盖 `.new`、`.rollback`、正式点存在/缺失、可唯一重建和歧义失败;任何成功恢复后只保留一个正式周检查点,任何歧义失败后现场对象不减少。
|
||||
- [x] orphan fail closed 通过本地合成链路证明 finalize 非零、源端不写成功状态、不执行源端保留清理且诊断为 `0600`;不重复执行真实企微断链验收。
|
||||
- [x] 现有同日取代、失败回滚、7 日 + 4 周保留、dead-man、备份/恢复往返、环境合同和敏感扫描均无回退。
|
||||
- [x] task、Trellis governance、文档注册/流程合同和 `git diff --check` 通过;只有经确认的任务文件进入提交。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 不新增数据库 migration,不重复部署 `2526052`,不修改生产业务数据。
|
||||
- 不重测已经通过的真实企微失败、重复降噪和恢复通知链路。
|
||||
- 不自动执行恢复,不删除 ARM 正式归档或手工批次,不改变 7 日 + 4 周保留策略。
|
||||
- 不改写仅含合成值的测试夹具 `env` 调用,不做全仓无关安全重构。
|
||||
- 不在本任务直接推送、部署、更新 ARM 脚本或复制恢复卡;这些动作分别保持外部门禁。
|
||||
- 不继续新增运维能力;本任务之后不因本轮审查再派生新的运维加固 task。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- Shell 语法检查和 `bash scripts/qa/run-automatic-backup-contract.sh`,扩展 mock argv/环境继承与 orphan 恢复矩阵。
|
||||
- `bash scripts/qa/run-backup-restore-drill.sh`,覆盖 Docker 备份与影子恢复入口无回退。
|
||||
- `node deploy/trialrun/validate-env-contract.js`。
|
||||
- `python3 ./.trellis/scripts/task.py validate .trellis/tasks/07-27-offhost-backup-final-fixes`。
|
||||
- Trellis governance、敏感扫描、文档注册、流程合同和 `git diff --check`。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "offhost-backup-final-fixes",
|
||||
"name": "offhost-backup-final-fixes",
|
||||
"title": "异机备份最终收尾修复",
|
||||
"description": "修复自动异机备份凭证进入进程 argv、单管理员恢复卡漂移和 finalize 强杀后孤儿事务恢复,完成后硬收口运维主线。",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": "scripts/run-automatic-backup.sh; scripts/backup-db.sh; scripts/restore-db.sh; scripts/receive-offhost-backup.sh; automatic-backup contracts; trialrun runbook/spec",
|
||||
"package": null,
|
||||
"priority": "P0",
|
||||
"creator": "shunli",
|
||||
"assignee": "shunli",
|
||||
"createdAt": "2026-07-27",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户