归档每日自动异机备份任务(07-27-automatic-offhost-backup)

这个提交包含在:
2026-07-27 17:32:52 +08:00
父节点 95b51e6245
当前提交 f1e8cf138a
共修改 7 个文件,包含 2 行新增和 2 行删除
@@ -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,85 @@
# 每日自动异机备份设计
## 总体边界
本任务不建设备份平台。生产主机负责生成和推送一个不可变成对批次,ARM 用受限 SSH 接收身份把传输区与归档区隔离;systemd 负责每日执行、缺失监测和季度提醒。
```text
production 02:30
resolve active Compose identity
-> flock
-> backup-all.sh (DB + storage + manifest)
-> local verify
-> rsync to ARM inbox/<run-id>.partial
-> restricted finalize <run-id> <manifest-sha>
-> ARM verify exact files/manifest/hash
-> atomic move to archive/<run-id>
-> daily + weekly retention
-> write safe success state
-> prune local daily copies
production 07:10 watchdog
-> recovery point missing/corrupt/stale/project drift ? fail + group alert
-> build SHA differs ? one finding + group alert
ARM independent watchdog
-> newest finalized archive <= 26h ? pass : direct group alert
-> source host/server/container not required
quarterly reminder
-> one redacted WeCom reminder per calendar quarter
-> human runs ARM shadow restore separately
```
## 生产备份编排
- 新增一个宿主 wrapper,所有正式输入由安装器写入 systemd:仓库根、Compose project/file/env、活动 PGDATA/附件目录、完整/短构建 SHA、备份根、状态根、SSH alias/key/known-hosts 和告警开关。
- wrapper 通过 Compose label 解析唯一 server/db 容器,并把实际 DB/附件 mount、server build SHA 与安装时冻结值逐项比较。任一漂移在调用 `backup-all.sh` 前停止。
- DB 名称/用户和密码只从活动容器环境在内存中取得,传给现有 Docker 备份路径;不得 source 漂移的旧部署 env,不得输出取值。
- 每个自动批次使用 `auto-<UTC timestamp>`,源端根和状态根均为显式安全绝对路径、权限 `0700`。`flock` 拒绝同一任务重入。
- 只有 ARM finalize 成功后才写 `last-success.json` 并清理源端旧日批次。失败保留本次完整源批次供重试,不产生异机成功状态。
## ARM 受限接收边界
- ARM 一次性安装 rsync,创建密码锁定的专用账号、独立 inbox/archive/weekly 目录和一把专用公钥。该公钥的 `authorized_keys` 强制执行仓库提供的 receiver,并禁用 PTY、端口转发、agent 转发和 X11 转发。
- receiver 只接受两类 `SSH_ORIGINAL_COMMAND`:rsync server 接收模式,或 `hyb-backup-finalize <run-id> <manifest-sha256>`。其他命令全部拒绝。
- rsync 通过 `rrsync -wo` 只能进入 inbox;archive 和 weekly 位于 rrsync 根之外。即使生产端密钥泄露,也不能用 rsync 读取或删除已归档批次。
- finalize 只接受 `auto-` 安全批次号和完整 manifest SHA。它要求 inbox 中恰有三个普通文件、无链接,manifest 的 runId/status/schema、相对文件名、大小和 SHA-256 全部匹配;同 run_id 重复 finalize 继续保持幂等。
- 同 UTC 日已有不同 run_id 时,先完整校验并冻结新批次权限,再把旧日归档 rename 到同文件系统临时回滚名、新批次 rename 到正式归档名;新批次发布失败立即把旧归档 rename 回原名。新批次发布成功后才删除旧回滚名,并重建该 ISO 周硬链接检查点,使每日一个恢复点和周检查点都指向当前有效批次。
- archive 保留最近 7 个日批次;每个 ISO 周首次成功批次用只读硬链接快照形成周检查点,保留最近 4 周。清理不得删除最后一个 archive 或当前 finalize 批次。
- 旧手工/审计批次不进入自动前缀和自动轮转范围。
## 传输与主机身份
- 生产 root 使用一把仅用于本接收器的私钥,权限 `0600`;SSH config 使用专用 alias,known_hosts 由人工核对的固定主机指纹生成,timer 固定 `StrictHostKeyChecking=yes`,不得使用 `accept-new`。
- rsync 只发送完整源批次到同名 `.partial` inbox。网络中断可续传同一 partial;远端 archive 同名存在时 finalize 失败,不覆盖。
- 仓库、任务和报告只记录 alias 类别、布尔值和批次数量,不记录地址、账号、密钥、公钥原文或完整运行目录。
## 告警与静默失败检测
- 新增一个小型宿主告警 helper,使用 Python 标准库从 root-only Webhook 文件读取完整 URL,并向企微运维群机器人发送固定白名单文本;不复用 OA server 容器、员工表或个人工号。
- 生产 wrapper 无论成功或失败都生成字段白名单 summary;即时失败和源端 watchdog 直接调用宿主 helper。Webhook 文件缺失、权限不是 `0600`、不是普通文件或内容格式不合法时 fail closed,并在本机诊断中留类别,不打印 URL。
- ARM receiver 安装器同时安装独立 dead-man service/timer。watchdog 读取 archive 中最新有效自动批次的完成时间,超过 26 小时、没有批次或 manifest 无法验证时直接调用同一 helper;状态文件实现首次失败、重复降噪和恢复通知。
- 生产 stale watchdog 仍验证 schema/status/project/时间。build SHA 不同输出 `PASS_WITH_FINDINGS`,不返回失败;恢复点缺失、损坏、未来时间、超过阈值或 project 不同继续返回失败。
- 生产外部命令的 stdout/stderr 只写状态目录下的临时诊断,失败时原子保留为 `last-error.log` 并设为 `0600`,成功后清理本次临时诊断。summary 和企微文本不得复制诊断正文。
- Webhook secret 同时存在于两台主机的 root-only 文件,仓库只保存“文件路径”合同;`.env.example` 和 systemd `Environment=` 都不得出现 URL/key。
- 季度提醒以年份+季度作为 finding key,确保每季度一次且同季度不重复;只提醒人工影子恢复,不执行恢复。
## 安装与回滚
1. 本地先实现合成测试、受限 receiver、告警适配和两个安装器,不接触生产/ARM。
2. 获批后先由用户在企微创建运维群机器人,并分别把同一 Webhook 写入两台主机 root-only 文件;不向 Codex、仓库或普通 `.env` 提供其内容。
3. 在 ARM 安装 rsync、受限账号/receiver/公钥、dead-man timer 和目标目录;使用无业务数据的合成批次验证拒绝 shell、同日安全取代和 dead-man 状态逻辑。
4. 再在生产安装专用私钥/known_hosts、每日备份 service/timer、stale watchdog 和季度提醒;首次安装先保持 timer disabled。
5. 手工启动一次获批自动备份,核对源/ARM 哈希、状态和无敏感输出;再 enable timers。
6. 真正停止一次源端 timer 或故意让一次传输失败,等待/模拟超过 26 小时并确认 ARM 群告警;恢复链路后验证恢复通知和新批次成功。
回滚只 disable/stop 新 timer、移除 unit 和专用密钥授权;保留已生成的完整备份、状态报告和 ARM archive,不删除历史数据。旧手工 `backup-all.sh` 入口保持兼容。
## 取舍
- 选择生产推送而非 ARM 拉取,是为了让一次任务同时掌握生成、传输、目标校验和即时告警结果。
- 不给生产普通 ARM shell,而用 inbox + 强制 receiver + archive 隔离,抵消推送方案最主要的跨机删除风险。
- 使用 rsync 而不是新备份服务;当前压缩批次规模较小,收益主要是断点续传和成熟 SSH 传输,不追求块级去重。
- 不自动恢复。季度人工影子恢复才是可恢复性证据,日 timer 只负责生成恢复点。
- 运维加固在 dead-man、真实失败链路、timer 和现有 runbook 恢复卡完成后硬收口;公司只有一名系统管理员,季度影子恢复由该管理员按卡自查,不设置第二人走查门禁。后续不继续新增平台化备份或告警能力,回到产品 backlog。
@@ -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,85 @@
# 每日自动异机备份实施计划
## 实施步骤
- [x] 实现宿主自动备份 wrapper:安全绝对路径、唯一 Compose 来源、活动 mount/build 校验、`flock`、成对备份、ARM 推送/finalize、成功状态和源端 7 日保留。
- [x] 实现 ARM 受限 receiver:只允许 rrsync 写 inbox 和 finalize,严格校验三文件 manifest/大小/哈希、原子归档、7 日 + 4 周保留和最后成功保护。
- [x] 实现生产与 ARM 安装器:专用账号/密钥/known_hosts 边界、systemd daily/watchdog/quarterly units、安装/卸载幂等和默认不触发真实任务。
- [x] 泛化现有企微运维告警的 stdin summary、issueId 和备份建议,使用独立状态文件并补单元测试。
- [x] 增加合成契约测试:来源漂移、锁冲突、不完整/损坏/重复批次、续传 partial、拒绝普通 shell、保留轮转、stale 和季度降噪。
- [x] 更新部署环境契约、runbook、checklist、管理员手册的最小操作入口和 `.trellis/spec/hyb-oa/deployment-trialrun.md`;不新增平行长文档。
- [x] 生成一页脱敏恢复卡内容并放在现有 runbook 的首部紧急段;由唯一系统管理员维护并用于季度自查,不另建知识库副本或第二人走查门禁。
- [x] 完成本地全量门禁并提交;生产、ARM 安装另行请求批准。
- [x] 获批后先安装 ARM 接收端并用合成批次验证,再安装生产 units,手工运行首次真实自动批次,最后 enable timers。
- [x] 获批执行一次可控源端停止告警并恢复正常链路;不读取附件正文、不执行业务或企微其他写入。
- [x] 删除个人工号默认值,新增生产/ARM 共用的 root-only 企微群机器人 helper;两端 systemd 只绑定 Webhook 文件路径。
- [x] 增加 ARM 26 小时 dead-man service/timer,覆盖缺批次、超龄、损坏、重复降噪和恢复通知。
- [x] 将 buildSha 不同降级为 finding;缺失/损坏/超龄/project 不同继续硬失败,并支持部署后立即运行一次备份。
- [x] 为 backup/rsync/finalize 保存 `0600` 最近失败诊断,summary 与群消息只保留脱敏类别。
- [x] 把同 UTC 日不同 run_id 从拒绝改为“完整校验后原子取代”,覆盖失败回滚和周检查点重建。
- [x] 增加 Webhook 不进入 `.env.example`/仓库配置/报告的否定断言,并在 runbook 记录调整 `BACKUP_RUN_AT` 后需人工确认次日窗口。
- [x] 本地门禁通过并完成只读复核后提交;不推送、不部署、不配置真实 Webhook。
## 预计文件边界
- 新增 `scripts/run-automatic-backup.sh`:生产 host 编排与 safe summary。
- 新增 `scripts/receive-offhost-backup.sh`:ARM forced-command receiver、finalize 和 retention。
- 新增 `deploy/trialrun/install-automatic-backup-timer.sh`:生产 units 安装/卸载。
- 新增 `deploy/trialrun/install-offhost-backup-receiver.sh`:ARM rsync/账号/目录/公钥安装检查;软件安装和账号创建只在获批远端阶段执行。
- 新增 `scripts/qa/run-automatic-backup-contract.sh`:纯合成正负向测试。
- 修改 `server/src/scripts/send-wecom-ops-alert.js` 及其单测,仅做通用脱敏 summary 兼容。
- 修改 `deploy/trialrun/validate-env-contract.js`、现有 runbook/checklist/管理员手册和部署规约。
- 新增一个不含 Secret 的宿主告警 helper;只读取安装时传入的 root-only Webhook 文件。
## 验证命令
- `bash -n scripts/run-automatic-backup.sh scripts/receive-offhost-backup.sh deploy/trialrun/install-automatic-backup-timer.sh deploy/trialrun/install-offhost-backup-receiver.sh scripts/qa/run-automatic-backup-contract.sh`
- `bash scripts/qa/run-automatic-backup-contract.sh`
- `bash scripts/qa/run-backup-restore-drill.sh`
- `cd server && node --check src/scripts/send-wecom-ops-alert.js`
- `cd server && npm test -- --run tests/unit/scripts/send-wecom-ops-alert.test.js`
- `node deploy/trialrun/validate-env-contract.js`
- `python3 ./.trellis/scripts/task.py validate .trellis/tasks/07-27-automatic-offhost-backup`
- `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`
- 合成测试必须模拟源端不再产生 finalize,并由 ARM dead-man 得到失败;不能只直接调用“发送消息”路径。
## 外部门禁与回滚点
- 安装 ARM rsync/账号/authorized_keys/目录属于远端配置写入,须单独批准;不得复用普通登录账号或本地工作站私钥。
- 安装生产 SSH key/config/systemd units、首次真实备份、真实异机传输和企微失败告警均须单独批准。
- 创建企微运维群机器人及把 Webhook 写入两台主机 root-only 文件由用户手工完成;Codex 不读取、不回显、不记录其内容。
- 首次真实执行前记录现有手工批次数量、磁盘余量、活动容器/mount/build 和现有 timer;只输出脱敏计数/布尔值。
- 任何目标身份、主机指纹、活动 mount、manifest 或哈希不匹配立即停止,不猜路径、不接受新指纹、不清理源端批次。
- 回滚新 timer/receiver 时只撤销调度和专用授权;ARM archive、源端完整批次和原 WARCH3 timer 保留。
## 本地实施与验证证据
- 已实现生产 wrapper、ARM forced receiver、独立群机器人 helper、ARM dead-man 和两端安装器;生产 3 组 timer 与 ARM 1 组 timer 均默认只安装为 disabled。
- `run-automatic-backup-contract.sh` 使用临时目录与 mock Docker/SSH/rsync 完成 46 项正负向合同,不连接真实主机;覆盖同日取代与回滚、坏候选保留、周检查点重建、Webhook 权限/链接/格式负例、build finding、状态损坏、源端停止、重复降噪和恢复通知。
- 企微告警聚焦单测 15 项通过;stdin 路径被替换为安全类别,AUTO_BACKUP 首次失败/季度提醒发送且相同重复状态降噪。
- `run-backup-restore-drill.sh` 本地合成 DB/附件往返通过,本次合成 RTO 为 8 秒;该结果不是生产 RTO 或 ARM 恢复证据。
- `scripts/check-minimal.sh` 通过:server 162 个测试文件、1004 项测试全部通过,client typecheck/build 通过。
- 环境合同、Trellis task/governance、敏感扫描、文档注册、流程合同和 `git diff --check` 均通过;文档注册保留既有 22 个未注册文件 warning,无本任务新增文档。
- 最终只读复核修正了三个本地缺陷:生产安装器残留个人工号校验、ARM 重装可能改变 dead-man 状态文件属主、stale 状态未校验 run id/manifest SHA;修正后 46 项合同与 `scripts/check-minimal.sh` 全量复跑通过。
- 更新后的 ARM dead-man/helper、生产 helper/runner、真实有效首批、四组 timer enable 与真实企微失败链路已在单独授权后完成;公司只有一名系统管理员,第二人季度走查不适用。
### 首次远端执行增量
- ARM 已安装 rsync、密码锁定专用账号和 forced-command receiver;从生产验证普通 shell、sender/read、delete 均拒绝,写通道 dry-run 通过且未生成归档。
- 生产三组 units 已安装为 disabled/inactive,并绑定活动 Compose project、PGDATA、附件挂载、构建标识和固定 SSH 身份。
- 首次真实备份曾在 `runtime_identity` 阶段因 Docker `println` 参数间空格 fail-closed;runner 改用 `docker inspect` mount JSON 后,后续真实源端成对批次已生成并传输。
- ARM finalize 随后因同 UTC 日已有不同 run id 而拒绝,因此尚无可作为自动备份闭环证据的新批次;本轮已在本地实现“完整校验后同日取代”,更新后的远端脚本、首批和 timer enable 仍待另行批准。
### 远端启用与真实告警验收增量
- 获批部署 `a0006f0` 后,生产活动构建与预期一致;自动备份同日新批次经 ARM 完整校验后安全取代旧日批次,源端与目标端 manifest、文件大小和 SHA-256 一致,最近失败诊断不存在。
- 生产 daily、watchdog、季度恢复提醒与 ARM dead-man 四组 timer 均为 `enabled/active`;最终只读复核中四个对应 service 最近结果均为 `success`、退出码均为 `0`。
- daily 下一窗口为每日 02:30,ARM dead-man 为每日 06:30、26 小时阈值,生产 watchdog 为每日 07:10,季度恢复提醒为每季度首日 09:00;失败验收结束后 daily 已恢复正常调度。
- 两端企微运维群机器人均由用户手工写入执行账号持有的 root-only `0600` 文件;Codex 未读取、回显或记录 Webhook 内容。
- 真实失败链路通过停止源端 daily timer 并临时缩短 ARM 阈值触发:首次 dead-man 超龄只发送一条 `FAIL_OFFHOST_DEADMAN`/`archive_stale` 脱敏通知,相同失败重复执行明确降噪、不再次发送。
- 恢复 daily、生成新的真实有效批次并恢复 ARM 正常阈值后,dead-man 返回 `success/0`,企微群只收到一条 `PASS_OFFHOST_DEADMAN_FRESH`/`healthy` 恢复通知;临时失败测试脚本已删除。
- 本轮外部验收未读取附件正文、备份文件正文、原始日志或 Webhook 内容,未修改业务数据;恢复卡已保存在现有 runbook,由唯一系统管理员按季度提醒自行执行影子恢复。
@@ -0,0 +1,76 @@
# 每日自动异机备份
## Goal
为 5–10 人内部 OA 建立低维护的每日自动恢复点:生产每天生成同批数据库、附件和 manifest,完整校验后复制到 ARM 备份机,失败可被当天发现并告警;同时用简单保留策略和季度人工影子恢复降低主机故障与静默备份失效风险。
## Background
- 现有 `scripts/backup-all.sh` 已能生成 DB、附件和脱敏 manifest 的完整批次,并在发布完整目录前验证 dump、tar 和 SHA-256。
- 当前仓库没有任何生产调度器调用 `backup-all.sh`;唯一活动 timer 是 07:30 运维巡检,不执行备份。
- 生产主机和 ARM 均有 systemd、SSH、`flock` 和 SHA-256 工具;生产有 rsync,ARM 当前没有 rsync。
- 两台主机之间当前没有可直接复用的免密 SSH 别名或密钥链路,不能把本地工作站的 SSH 配置误当作服务器间自动化能力。
- 当前手工正确批次和 ARM 影子恢复已证明格式可用,但不能满足 RPO 24 小时或自动失败发现。
- 已确认采用“生产主动推送到 ARM 的专用受限接收身份”:生产保留即时传输结果,ARM 的已归档批次不暴露给 rsync 写入根。
- 任务实施前,生产切换已完成并应用 61 项 migration;本地为 62 项,`2526052` 后续部署只新增 1 项 migration。当时自动备份三组 units 已安装但尚未形成可靠启用闭环。
- 首次真实自动批次曾证明源端成对备份可生成,但 ARM 因“同一 UTC 日仅允许一个不同 run_id”拒绝重试批次;当时即时告警仍依赖活动 server 容器,不能覆盖容器或整机故障。
## Scope
- 生产每日成对备份、ARM 受限接收与目标端校验。
- 源端 7 个日批次、ARM 7 个日批次加 4 个周检查点的安全轮转。
- 每日 systemd 调度、独立 stale watchdog、失败企微告警和季度人工恢复提醒。
- 两端安装器、合成契约测试、现有运维文档/规约更新和一页脱敏恢复卡。
## Requirements
- 每日自动从唯一活动 Compose project 解析 DB 容器、活动附件挂载和实际构建标识,不读取旧项目、固定容器名或仓库默认附件目录。
- 复用 `backup-all.sh` 生成完整批次;任何来源核验、dump、tar、manifest 或哈希检查失败时不得进入异机完成态。
- 使用独立 systemd service/timer 和 `flock`,避免与部署、人工备份或自身上次运行重叠。
- 完整批次通过固定主机指纹和专用受限密钥自动推送到 ARM;该身份无普通 shell,只能把文件写入 inbox 或请求校验/归档,不能通过 rsync 读取或删除 archive。
- 传输端和目标端都要校验 manifest、文件大小和 SHA-256;同名已归档目标不得覆盖。中断传输可以在 inbox 内续传,但只有完整校验后才能原子进入 archive。
- 生产保留最近 7 个成功日批次;ARM 至少保留最近 7 个日批次和 4 个周检查点。清理只能发生在本次异机成功确认之后,且不得删除最后一个已知成功批次。
- 生产即时告警和 ARM dead-man 告警统一使用独立企微运维群机器人;Webhook 只从 root-only 文件读取,不依赖 OA server、数据库、员工绑定或默认个人工号,也不得进入仓库、`.env.example`、Trellis、日志或聊天。
- ARM 安装独立 26 小时 dead-man timer,直接检查最近归档完成时间;生产整机或容器不可用、timer 未运行、传输未 finalize 时仍能从另一台主机发出告警。
- 生产 watchdog 的“恢复点缺失/损坏/超龄/Compose project 不匹配”继续硬失败;仅 `buildSha` 早于当前部署时降级为一次 finding,不得误报为没有恢复点。部署后仍推荐立即触发一次获批备份。
- backup、rsync 和 finalize 的失败输出必须写入状态目录下权限 `0600` 的最近失败诊断;企微告警仍只包含阶段、状态、finding 数和脱敏建议,不附原始输出。
- 同一 UTC 日已有不同自动归档时,新批次必须先完整通过 manifest、文件集、大小和 SHA-256 校验,再以可回滚的同文件系统 rename 顺序原子取代旧日批次;任一替换步骤失败必须恢复旧批次或保留可诊断状态。最终仍只保留每日一个恢复点。
- 告警只包含阶段、状态、finding 数和脱敏建议,不包含数据库凭证、完整目录、SSH 信息、真实业务数据、附件名或原始命令输出。
- 季度恢复保持人工触发,在 ARM 隔离目标执行;季度 timer 只发送一次脱敏提醒,不得自动运行恢复,更不得触碰生产库。
- 形成一页脱敏恢复卡,说明主机故障后的负责人、备份位置类别、恢复顺序和验收项,保存在现有运维 runbook 中,由唯一系统管理员维护并用于季度自查。
## Acceptance Criteria
- [x] 本地合成测试证明每日 wrapper 只接受唯一活动来源、互斥锁有效,并只发布完整备份批次。
- [x] 传输测试证明不完整批次、哈希不一致、重复批次和不可达 ARM 均失败,且不产生异机成功状态。
- [x] 保留测试覆盖 7 个日批次、4 个周检查点、最后成功批次保护和仅在异机确认后清理。
- [x] 告警测试证明首次失败和状态恶化会告警,重复同一失败降噪;07:10 stale 检查能发现超过 24 小时没有异机成功记录。
- [x] 本地合成测试证明同日不同 run_id 的完整新批次可取代旧批次,损坏新批次不会影响旧归档,且周检查点与日归档不产生悬空或错误指向。
- [x] 本地合成测试证明 buildSha 不同仅产生 finding,恢复点缺失/损坏/超龄/project 不同仍失败;失败诊断为 `0600` 且不进入告警摘要。
- [x] 环境契约证明 Webhook 相关键和值不出现在 `.env.example`、仓库配置或报告;两端安装器只接受 root-only Webhook 文件路径。
- [x] ARM 26 小时 dead-man 合成验证覆盖“源端停止/不产生新 finalize”而非仅验证机器人能发送,并覆盖首次失败、重复降噪与恢复通知。
- [x] systemd 安装器显式绑定 Compose project、活动数据路径、构建标识、备份根和受限远端身份;安装后四组 timer 均 enabled/active,安装阶段未手工执行真实告警。
- [x] 经单独生产授权后完成一次自动日备份:源端完整、ARM 目标端哈希一致、状态报告脱敏,且未读取附件正文或修改业务数据。
- [x] 模拟源端停止产生一条脱敏企微告警,相同失败重复执行不再发送;恢复正常链路并完成成功批次后只发送一条恢复通知。
- [x] 恢复卡已写入现有运维 runbook,覆盖恢复入口、影子目标和 `/ready`/关键计数验收项;公司只有一名系统管理员,不设置虚构的第二人走查门禁。
## Out of Scope
- 不引入备份平台、对象存储、MinIO、云厂商 SDK、Kubernetes 或持续复制。
- 不自动执行生产恢复、数据库 reset、迁移改写或附件回写。
- 不把真实业务正文、附件清单、凭证、主机地址、完整目录或 SSH 私钥写入仓库、报告或聊天。
- 不把企微群机器人 Webhook URL、key 或配置文件正文写入仓库、`.env.example`、Trellis、报告、日志或聊天。
- 不在本任务继续企微本人授权、员工角色、付款、报销或其他产品功能。
- 不因自动备份完成而取消季度人工影子恢复。
## Test Plan
- Shell 语法与合成文件系统测试,覆盖锁、来源解析、传输失败、目标重复、哈希和保留轮转。
- 合成 receiver 测试覆盖同日完整批次取代、损坏候选保留旧批次、替换失败恢复以及周检查点一致性。
- 合成双主机告警测试覆盖生产无 server 容器、ARM 26 小时无新归档、重复降噪、恢复通知和 root-only Webhook 文件门禁。
- 复用 `scripts/qa/run-backup-restore-drill.sh` 验证成对备份合同未回退。
- 聚焦测试现有企微运维告警的备份状态与 stale finding 适配。
- `node deploy/trialrun/validate-env-contract.js`。
- `python3 ./.trellis/scripts/task.py validate .trellis/tasks/07-27-automatic-offhost-backup`。
- Trellis governance、敏感扫描、文档注册/流程合同和 `git diff --check`。
- 生产安装、首次自动批次、真实失败告警和 ARM 目标验证分别保持外部门禁。
@@ -0,0 +1,32 @@
# 自动异机备份运行拓扑调查
## 边界
- 调查日期:2026-07-27。
- 仅执行只读命令;未创建账号、密钥、目录、timer 或备份,未安装软件、传输数据或发送告警。
- 只记录工具可用性、批次数量和连接布尔值,不记录主机地址、账号、完整路径、凭证或业务数据。
## 仓库事实
- `scripts/backup-all.sh` 只被手工文档和合成恢复演练调用,没有生产调度入口。
- 完整批次由 `database.dump`、`storage.tar.gz` 和 `manifest.json` 组成;最终目录只在 dump/tar 可读取且 SHA-256 生成后发布。
- DB Docker 路径要求显式容器和数据库参数;附件路径要求显式 `STORAGE_DIR_HOST`,不存在历史默认回退。
- 07:30 WARCH3 timer 只运行每日巡检。现有告警 CLI 可读取通用 summary 的 `issueId`、`status`、`findingCount` 和报告目录,但帮助、状态文件和部分建议文案仍绑定 WARCH3,备份复用前需要小范围泛化或独立状态文件。
## 主机能力
| 能力 | 生产主机 | ARM 备份机 |
| --- | --- | --- |
| systemd / SSH / flock / SHA-256 | 可用 | 可用 |
| rsync | 可用 | 当前不可用 |
| 现成跨主机免密别名/链路 | 无 | 无 |
- 生产当前存在少量历史/手工批次,但没有定时策略。
- 正确异机批次此前已复制并完成影子恢复;本轮默认目录探测没有把它当作新自动化根,说明自动化必须显式配置目标根,不能猜历史位置。
## 设计影响
- 无论推送或拉取,都需要一次性安装/配置 ARM rsync 并建立专用受限身份,不能复用本地工作站密钥作为生产事实。
- 直接给生产保存 ARM 普通登录密钥虽然最省事,但会让生产失陷扩展为异机备份可删除,不应采用。
- 生产推送更容易在同一任务内获得传输退出码并触发即时告警;要用无交互 shell、仅允许新批次写入的受限接收身份,并让已归档批次脱离该身份的删除权限。
- ARM 拉取让生产主机没有 ARM 凭证,隔离更强;代价是调度和告警分散在两台机器,需要额外心跳或状态回传才能保证当天发现失败。
@@ -0,0 +1,26 @@
{
"id": "automatic-offhost-backup",
"name": "automatic-offhost-backup",
"title": "每日自动异机备份",
"description": "为内部 OA 建立每日成对备份、ARM 异机复制、保留轮转、失败告警与季度恢复提醒的低维护自动化基线。",
"status": "completed",
"dev_type": null,
"scope": "scripts/backup automation; deploy/trialrun backup receiver and systemd installers; server ops-alert adapter/tests; deployment runbook/checklist/spec evidence",
"package": null,
"priority": "P0",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-07-27",
"completedAt": "2026-07-27",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}