文件
hyb-oa/.trellis/tasks/08-02-deployment-preflight-backup-contract-refresh/prd.md
T

10 KiB

部署前聚合体检与备份合同自动刷新

Goal

让已有部署在任何远端写入前一次性看清合同、权限、数据挂载和备份构建身份的全部漂移,并在应用切换成功后自动刷新备份/WARCH3 service 的构建标识。部署不再依赖操作者逐个撞 fail-fast 护栏,也不再依赖人工记住“部署后重装 timer”。

Background

  • 2026-08-02 的受控部署连续暴露三个历史漂移断面:冻结的 REMOTE_DIR 不是仓库根、root 维护身份与部署身份未按实际通路区分、冻结的环境文件路径不可由部署用户读取。现有护栏串行 fail-fast,因此每修复一处才看到下一处。
  • 同一轮恢复已完成一次全量对账:六键合同、163 个生产环境键、运行容器、PGDATA/附件挂载、4 个 service、4 个 timer、真实备份和异机恢复点均已对齐并跑绿。本任务把该对账方法变成可重复机制。
  • deploy.sh 只把新 BUILD_SHA 注入应用容器,不重装 automatic-backup/WARCH3 units;备份 runner 又要求 unit 中冻结的 build 与活动容器一致。人工漏做刷新时,应用可正常运行而自动备份静默 fail-closed。
  • 仓库已有 forced-command 先例:异机备份接收端以 root-owned 可执行文件配合 SSH_ORIGINAL_COMMAND 白名单和 SSH 转发/PTY 禁用选项限制授权面。

Scope

  • 为已有部署增加一次性汇总的只读体检,并在任何远端写入前阻断全部已发现的合同、权限、挂载和 build 漂移。
  • 建立独立 forced-command SSH 身份与 root-owned 自包含刷新程序,只更新四个固定 service 的 build drop-in并验证四个既有 timer。
  • 把聚合体检接到 deploy-existing.sh 前置,把自动刷新接到完整部署与公网检查之后,并补齐测试、runbook、checklist 和部署 spec。
  • 保留 deploy.sh 首次初始化/应急路径和既有 installers;生产安装、authorized_keys 修改和首次启用另行批准。

Requirements

1. 只读聚合体检

  • 新增已有部署专用体检入口,由 deploy-existing.sh 在任何 mkdir、rsync、Compose、systemd 或其他远端写入前调用。
  • 体检至少覆盖:六键合同完整;REMOTE_DIR 是完整仓库根且部署用户可写;REMOTE_ENV_FILE 为普通文件且部署用户可读;PGDATA 含 PG_VERSION;附件目录存在;运行中唯一 server/db 容器与合同中的 build、PGDATA、附件挂载一致;automatic-backup unit 的 BUILD_SHA 与活动 server 一致;普通部署 SSH 身份可用;独立 root forced-command 身份可用且收尾程序存在、可执行。
  • 主流程不得使用 set -e 实现检查遍历。每个检查必须在独立子 shell 中执行并显式收集退出码;前一项失败不能阻止无依赖的后续项执行。依赖事实缺失时,相关项仍以固定检查名报告 FAIL,不得静默跳过。
  • 输出只包含固定检查项名、PASS/FAIL、汇总计数和固定错误分类;不输出 SSH 别名、主机、路径、env 值或散列、容器 ID、密钥、unit 原文及原始命令响应。
  • 任一必需检查失败,体检最终退出非零,deploy-existing.sh 不得继续同步或部署。现有 fail-fast preflight-remote.sh 保留为第二道防线。

2. 独立 root forced-command 通路

  • 使用独立维护 SSH 身份,不以 sudo -n true 代表 root 能力,不新增 NOPASSWD sudoers。
  • 专用公钥必须只以 forced-command 形式授权;同一 key blob 若还存在不受限 root 授权,安装器必须 fail-closed。不得复用当前不受限 root 管理密钥来制造“看似受限”的入口。
  • forced-command 指向 /usr/local/sbin 下 root-owned、非符号链接、0755 的自包含程序;程序不得 source、exec 或调用 REMOTE_DIR 及其他部署用户可写目录中的脚本、二进制或配置。
  • SSH 入口禁用 agent forwarding、端口转发、X11、PTY 和 user rc;只接受 SSH_ORIGINAL_COMMAND 的精确白名单子命令,首版仅允许无附加参数的 probe 与 refresh。

3. 部署后自动刷新

  • deploy-existing.sh 只有在应用部署、/ready、runtime/external smoke 与公网检查成功后,才通过独立 root forced-command 身份调用 refresh。
  • root 程序从 root-owned systemd 合同和唯一活动 server 容器读取 Compose 身份及完整 BUILD_SHA,严格校验后仅更新四个固定 service 的 build 环境覆盖:automatic-backup daily、watchdog、drill-reminder 与 WARCH3。不得接受调用方传入路径、unit 名、SHA 或任意命令。
  • 刷新必须原子写入 root-owned systemd drop-in,执行 daemon-reload,再验证四个 service 的最终 BUILD_SHA/VITE_BUILD_SHA 与活动容器一致,以及四个既有 timer 均为 enabled/active。不得调用仓库内 installer,也不得顺带重写其他 unit 字段。
  • 刷新失败时 deploy-existing.sh 必须退出非零并明确标记“应用已部署但合同未收口”,不得打印普通成功结论,也不得自动回滚已通过 smoke 的应用。
  • 对外固定区分四类失败:root SSH 身份不可用;收尾程序不存在或不可执行;程序完成但 unit SHA 未更新;unit 已更新但 timer 未 enabled/active。

4. 安装、兼容与生产边界

  • 提供一次性 root 安装器,将经过审核的自包含程序复制为 root-owned 文件并安装/更新 managed forced-command 授权行;安装器本身仍需显式 root 执行,不进入免密授权。
  • 保留现有 automatic-backup/WARCH3 installers 作为首次安装、路径/完整合同变化和人工修复入口;自动收尾只覆盖每次发布必变的 build 标识。
  • 现有 deploy.sh 首次初始化/受控应急入口不强制依赖 forced-command;自动体检与刷新只接入 deploy-existing.sh。
  • 生产安装 forced-command、修改 root authorized_keys 或首次启用自动收尾均属于生产写入,实施完成后必须另行批准;本任务规划批准和代码批准不授权远端安装。

Acceptance Criteria

  • 合成场景同时制造至少两个前置失败时,体检报告包含全部预期 FAIL 项,且最后一个独立检查确实执行;不能因 set -e、pipefail 或函数非零提前退出。
  • 体检全绿时只输出固定项名和 PASS/汇总;敏感扫描与测试证明没有路径、主机、env 值/散列、容器 ID 或原始响应。
  • 六键缺失、仓库根错误、env 不可读、源码根不可写、PGDATA/附件缺失、实际挂载漂移、unit build 漂移均能在远端写入前一次汇总发现。
  • 普通部署身份和 root forced-command 身份分别验证;root transport 不可用与远端程序不存在/不可执行得到不同固定分类。
  • forced-command 只接受精确 probe、refresh;空命令、带额外参数、其他子命令均拒绝。安装器拒绝同一公钥同时存在不受限 root 授权。
  • 安装后的 root 程序为 root-owned、0755、非符号链接,且静态/动态测试证明不执行仓库或其他部署用户可写路径中的程序。
  • 成功 refresh 从活动容器取得 build,原子更新四个固定 service 的 drop-in,四个最终 SHA 一致且四个 timer enabled/active;调用方无法指定 SHA、路径或 unit。
  • 四类收尾失败分别有回归:SSH root 身份不可用、程序不存在/不可执行、unit SHA 未更新、timer 未运行。
  • 任一收尾失败发生在应用已切换后,deploy-existing.sh 退出非零并输出固定“已部署但未收口”状态;成功时才输出部署完整完成。
  • deploy.sh 直接初始化/应急路径保持兼容;automatic-backup/WARCH3 既有 installer、真实备份与 watchdog 行为不被改写。
  • shell syntax、部署安全单测、环境合同、聚合体检 fixtures、forced-command fixtures、Trellis/governance/process/sensitive/diff 门禁全部通过。

Out of Scope

  • 不建设 CI/CD、蓝绿发布、自动回滚或新的通用远程执行框架。
  • 不把体检挂成每周 timer;先观察它在后续真实部署中是否持续发现漂移,再单独评估第 5 个 unit。
  • 不在本任务删除四个旧源码目录、旧环境文件或其他历史回退材料;体检跑绿也只形成后续清理的申请前提。
  • 不通过 SYNC_ENV=1 修复环境文件,不自动挑选、复制或轮换生产 Secret。
  • 不让 root 收尾程序修改 Compose、数据库、附件、业务数据、备份制品或执行真实恢复。

Deferred Decision With Trigger

  • 每周只读体检 timer 暂不建设。触发条件:本脚本上线后在后续真实部署中再次发现非一次性漂移,或出现人工改服务器导致合同在部署间隔内失配;届时单独立项评估定时运行、告警和第 5 个 unit 的维护成本。
  • 旧目录/旧环境文件清理单独审批。至少要求聚合体检在规范根上跑绿、一次后续真实部署自动收尾跑绿,且当前异机备份仍新鲜。
  • root-owned helper 自身摘要比对暂不加入首版协议。触发条件:生产安装后的 helper 与仓库审核版本出现过内容漂移,或后续需要把 root-owned 独立制品纳入统一完整性巡检;届时单独设计可信摘要来源、升级与回滚合同,不能让 helper 自报摘要后由自身证明可信。

Test Plan

  • bash -n 检查新增/修改的所有部署 shell。
  • 扩展 server/tests/unit/scripts/deployment-safety-contract.test.js,锁定 preflight 在任何远端写入前、刷新在应用与公网检查后、失败必须向上传播。
  • 新增纯 fixture 合同测试,使用假的 SSH/systemctl/docker 响应覆盖聚合不短路、脱敏输出、身份/程序四类失败、固定 unit/drop-in 和 timer 验证。
  • 运行 node deploy/trialrun/validate-env-contract.js、相关 automatic-backup 合同测试及既有部署安全回归。
  • 运行 Trellis task validate、governance、process contract、docs registry、sensitive content 与 git diff --check。

Notes

  • Keep prd.md focused on requirements, constraints, and acceptance criteria.
  • Lightweight tasks can remain PRD-only.
  • For complex tasks, add design.md for technical design and implement.md for execution planning before task.py start.