加固部署隔离与备份来源契约(07-26-internal-ops-recovery-baseline)

这个提交包含在:
2026-07-27 06:13:02 +08:00
父节点 130518f471
当前提交 23342035ee
共修改 16 个文件,包含 304 行新增和 80 行删除
@@ -112,14 +112,19 @@ GET /auth/wecom/login?mode=onboard
- 现场是混合部署,不能把“统一目录”处理成普通 rsync:必须以当前活动 PostgreSQL 数据和附件挂载为唯一数据事实源。
- 在停机切换前固定记录活动容器、数据挂载、环境文件摘要、端口、Caddy 路由和回滚镜像;不得打印环境变量内容。
- 当前部署脚本固定探测宿主端口,而现场通过另一宿主端口提供 OA;脚本必须从已验证的 Compose/env 配置解析实际端口,不能硬编码。
- 现场通过非默认宿主端口提供 OA;部署脚本必须从已验证的 Compose/env 配置解析实际端口,不能回退到硬编码(本地已实现,待获批部署)。
- 两套目录的 Compose 容器名和环境文件不同。切换前必须先选择唯一目录并证明其 DB/附件目标就是当前活动数据;不得让 Compose 创建第二套空数据库后误判部署成功。
- 统一后应由同一目录负责 source sync、Compose、环境文件和运维 timer,并让 `/ready`、前端 marker 和容器环境同时暴露同一短构建标识。
- 任一迁移状态、数据挂载、端口或公网构建标识不匹配时,立即切回旧容器和 Caddy 目标;不执行数据库 reset 或数据目录覆盖。
- Compose 不再固定 `container_name`;deploy 与 timer 通过 `-p` 显式选择同一项目,PostgreSQL 与附件宿主路径分别由 `POSTGRES_DATA_DIR`、`STORAGE_HOST_DIR` 参数化。
- 项目名隔离不是独立部署步骤。项目名、容器身份和数据目录必须在同一次停机切换中启用;旧实例未停止时不得让新 PostgreSQL 挂载同一 PGDATA。
- DB 不发布宿主端口;应用仅通过 Compose 内部网络访问 `db`。环境契约必须拒绝 DB `ports`、固定容器名或未参数化数据目录回退。
- Docker 备份/恢复必须显式传入由 `compose ... ps -q db` 得到的已核验容器 ID;附件备份必须显式传入活动挂载源,两个脚本都不猜默认值。
- 远端 `/ready` 构建标识在 server 容器内用 Node 校验,宿主机只负责 curl 和 Compose,不要求安装 Node。
### 生产统一顺序
1. 使用已完成的成对异机备份作为灾难回滚点,并记录活动数据挂载、环境摘要、容器镜像和 Caddy 端口。
1. 另行获批后,从活动 DB 和活动附件挂载源重新生成成对异机备份作为灾难回滚点;旧批次因混入非活动附件目录只保留审计,并记录活动数据挂载、环境摘要、容器镜像和 Caddy 端口。
2. 本地先修复 Compose 可配置回环端口及部署脚本动态端口探测,通过全部门禁并提交、发布批准提交。
3. 停机后把非活动的既定部署目录改名保留,将活动数据目录和环境文件迁入既定目录;同一文件系统优先原子改名,不复制运行中的 PostgreSQL 文件。
4. 用新 Compose 重建 db/init/server,应用已提交 migrations;旧容器和旧目录在验收前保留,不清理。
@@ -10,7 +10,8 @@
- [x] 更新 runbook/checklist 和部署规约,区分普通备份、影子演练和真实事故恢复。
- [x] 运行任务测试计划和 Trellis 治理门禁;按实际结果记录本地 RTO,不伪造 `/ready` 或异机证据。
- [x] 在单独授权后完成真实运行来源、职责分离、异机副本和 `/ready` 的只读核查,并如实记录 `NO_GO` 缺口。
- [x] 经明确授权完成提交发布和真实 DB+附件+manifest 成对异机备份,两端完整性复验通过。
- [x] 经明确授权生成并复制过 DB+附件+manifest 批次;后续发现附件来自非活动目录,已将该批次降级为无效恢复证据并保留审计。
- [ ] 经后续明确授权,从活动 DB 与活动 server 附件挂载源重新生成正确批次并完成异机复验。
- [ ] 经后续明确授权完成真实职责分配、部署/迁移和真实格式影子恢复后复验。
- [x] 修复企微 HR 双 Secret 分工:通讯录 token 只列 userid,自建应用 token 读取详情、部门和 HR 信息。
- [x] 补充单元测试并运行后端语法、聚焦测试、Prisma 与 Trellis 门禁。
@@ -19,6 +20,8 @@
- [x] 增加默认关闭的一次性企微敏感资料授权入口,保持普通登录静默;实现 `user_ticket -> getuserdetail`、active/字段/唯一性门禁和仅 `employee` 自助开通。
- [x] 补齐 OAuth 路由、服务、配置契约和单元测试,证明票据/原始敏感响应不进入日志、Cookie、数据库或审计。
- [ ] 修复部署脚本的现场端口解析,确定唯一部署目录,并在活动 DB/附件挂载、环境摘要和旧镜像回滚点全部确认后才执行获批切换。
- [x] 完成本地 Compose/备份加固:移除固定容器名和 DB 宿主端口,参数化数据目录,deploy/timer 显式 project,备份来源缺失硬失败,远端构建标识改由容器内 Node 核验。
- [x] 增加隐式附件源、隐式 DB 容器、DB 端口与固定容器名回退的负向契约演练;未部署或启用新 Compose。
- [ ] 部署后验证唯一 Compose 来源、构建标识、迁移状态、Caddy 公网 `/ready` 和前端 marker;不因服务可访问而忽略来源漂移。
- [ ] 用户在企微应用详情选择手机、邮箱、企业邮箱敏感字段;部署开关开启后,两名目标分别通过一次性链接本人确认授权。
- [ ] 两人自助开通成功后写入获批 `finance` / `ceo` 职责,复核不同身份、maker-checker、会话撤销、审计和付款零写入。
@@ -46,6 +49,7 @@
- `cd server && npm test -- --run tests/unit/routes/wecomAuth.test.js tests/unit/wecomHrMaster.service.test.js`
- `cd server && npm test -- --run tests/unit/routes/wecomAuth.test.js tests/unit/wecomHrMaster.service.test.js tests/unit/wecomOAuthOnboarding.service.test.js`
- `bash -n deploy/trialrun/deploy.sh`
- `bash -n deploy/trialrun/install-warch3-ops-timer.sh`
## 本轮本地验证证据(2026-07-26)
@@ -53,6 +57,13 @@
- `prisma validate`、四个企微改动文件 `node --check`、部署环境契约和部署脚本语法均通过。
- 本地实现没有开启 `WECOM_OAUTH_ONBOARDING_ENABLED`,没有部署、迁移、写真实员工/角色或修改企微后台权限。
## 本地部署加固证据(2026-07-27)
- Shell 语法、部署环境契约、Compose 示例配置解析、Trellis task/governance、敏感扫描、文档注册、流程合同、Prisma 和 `git diff --check` 通过。
- 合成备份恢复在 Node 路径和移除 Node 的 Python 3 回退路径均通过,最近两次本地参考 RTO 均为 6 秒;负向覆盖隐式附件源、隐式 DB 容器、DB 宿主端口、固定容器名和 deploy 缺失 project name。
- 本阶段授权仅覆盖本地文件、测试、规约和提交,不包含生产备份、停机、迁移、部署、timer 安装或企微写入。
- 已确认旧异机批次混用了活动 DB 与非活动附件目录,保留审计但不再作为活动实例成对恢复证据;正确批次须另行批准重做。
## 回滚点
- 成对入口失败:保留四个底层脚本,不写完整 manifest。
@@ -43,6 +43,7 @@
- 自助开通按企微 userid、手机号和邮箱做唯一性检查;新账号只创建 `employee` 角色,随机不可知密码并要求改密,不自动授予财务、CEO、管理员或审批角色。
- 普通 `snsapi_base` 登录、已绑定账号登录和现有绑定流程保持兼容,不因资料授权功能改成每次手工确认。
- 生产统一以仓库既定部署目录和 Compose 契约为目标,但活动 PostgreSQL 与附件数据是唯一数据事实源;停机切换不得复制运行中的 PostgreSQL 文件、启动第二套空库或覆盖环境变量。
- Compose 项目名、容器身份与 DB/附件目录必须在同一次获批停机窗口切换;DB 不发布宿主端口,备份不得猜测容器名或附件目录。
- 部署健康检查必须从 Compose 解析实际宿主端口,并校验后端 `/ready`、前端 marker 与批准提交一致;应用已提交的 Prisma migrations 但禁止 reset 或改写历史。
## Acceptance Criteria
@@ -60,6 +61,7 @@
- [x] 普通企微登录仍生成 `snsapi_base`,一次性开通入口生成 `snsapi_privateinfo`;缺少开关、`user_ticket`、成员授权字段、active 状态或唯一性时均拒绝创建(本地 synthetic 验证)。
- [ ] 一次性授权通过 `getuserdetail` 获取成员同意的手机和邮箱,自动创建或补齐 OA 员工与企微绑定;新建账号角色严格等于 `employee`,日志和审计不含票据或完整敏感响应。
- [ ] 部署目录、Compose、环境文件、运维 timer 和活动容器来源统一;活动 DB/附件连续,远端 migrations 与批准提交一致,公网 `/ready` 和前端 marker 返回同一短构建标识。
- [x] 本地 Compose、部署、timer 和备份合同完成加固并覆盖负向回退;未把本地通过误报为生产切换完成。
- [ ] 两名目标成员分别完成一次本人授权后,账号唯一、企微绑定不同,再由管理员写入 `employee + finance` 与 `employee + ceo`;maker-checker、会话撤销和角色审计通过。
## Out of Scope
@@ -84,7 +86,7 @@
- `cd server && npm test -- --run tests/unit/wecomHrMaster.service.test.js`
- `cd server && npm run prisma:validate`
- `cd server && npm test -- --run tests/unit/routes/wecomAuth.test.js tests/unit/wecomHrMaster.service.test.js`
- `bash -n deploy/trialrun/deploy.sh`
- `bash -n deploy/trialrun/deploy.sh deploy/trialrun/install-warch3-ops-timer.sh`
- `node deploy/trialrun/validate-env-contract.js`
- 获批部署窗口内执行迁移状态、Compose 数据挂载、`/ready`、前端 marker、Caddy 公网路由和回滚容器只读复验。
@@ -105,11 +107,11 @@
- 获批只读核查结果见 `research/live-read-only-baseline.md`,结论为 `NO_GO_FOR_FORMAL_INTERNAL_USE`。
- 公开 HTTPS、TLS、HSTS 和 `/ready` 当前可用,但运行环境没有后端/前端构建标识,且远端迁移少于本地当前基线。
- 真实角色只有一个活跃账号,财务和付款复核候选均为 0,不满足 maker-checker。
- 只读核查时的历史 DB/附件备份约 55 天未更新、无 manifest,且与运行数据位于同一设备;获批后已生成新的 DB+附件+manifest 完整批次并复制到独立主机,两端校验一致。
- 只读核查时的历史 DB/附件备份约 55 天未更新、无 manifest,且与运行数据位于同一设备;后续异机批次虽通过传输校验,但复核发现混用了非活动附件目录,已降级为无效恢复证据,须另行批准后从活动 DB/附件重新生成。
- 当前代码已在获批后以普通快进方式发布到 upstream,来源门禁为 `PASS`;尚未执行角色修改、部署、迁移或真实格式影子恢复,因此最后一项验收继续保持未勾选。
- 当前 OA 是宿主机 Caddy、容器化 server/frontend、独立 PostgreSQL 容器和宿主附件挂载组成的混合部署;公网 OA 路由确实进入 server 容器。
- 服务器存在两套无 Git 元数据的部署目录:运行容器来源与日常运维任务指向不同目录,Compose 和环境文件也不一致;运行代码与本机当前代码不一致且缺少构建标识。
- 现场端口与当前部署脚本的固定健康检查端口不一致;未统一活动数据目录、容器名、端口和环境文件前,不得直接运行现有部署脚本。
- 现场端口与历史部署脚本的固定健康检查端口不一致;本地脚本现已动态解析,但未统一活动数据目录、项目名、端口和环境文件前仍不得部署。
- 企微结构化探测证明 6 名成员的批量详情接口均成功并返回部门,但手机号、邮箱和企业邮箱可读数量均为 0;HR 接口均成功但没有 `staff_info`。官方文档确认这不代表字段不可获取:2022-06-20 后新建自建应用应由管理员在应用详情选择敏感字段,再由成员通过 `snsapi_privateinfo` 手工授权,并用 `user_ticket` 调 `/cgi-bin/auth/getuserdetail` 获取本人手机号、邮箱或企业邮箱。
- 当前 OA OAuth 只使用 `snsapi_base`,且没有调用 `/cgi-bin/auth/getuserdetail`,因此现有登录链路不能采集敏感字段。两名目标各唯一匹配且身份不同,但批量 dry-run 均缺 `phone`、`email`,真实写入已按门禁停止。
- 本轮已在本地实现默认关闭的一次性 `onboard` 入口;普通登录继续静默。全量后端门禁与安全补修后的企微聚焦回归结果记录在 `implement.md`。该证据不代表功能已部署,也不代表企微后台敏感字段已由管理员选择。
@@ -63,30 +63,30 @@
服务当前可访问不等于运行与恢复底线通过。进入下一步前必须按顺序完成:
1. 已将当前获批代码以普通快进方式发布到 upstream,来源门禁复验为 `PASS`。
2. 已在获批后用新成对备份合同生成 DB + 附件 + manifest,并复制到独立主机;两端 manifest 和产物校验通过。
2. 重新从活动 DB 与活动 server 附件挂载源生成 DB + 附件 + manifest,并复制到独立主机;此前批次混用了非活动附件目录,不能作为活动实例的成对恢复证据。
3. 明确至少一名财务处理人和另一名 `manager`/`ceo` 付款复核人;系统管理职责单独分配,应急 `admin` 独立托管。
4. 通过获批部署任务升级代码和 3 项迁移,验证 `BUILD_SHA`、前端 marker、`/ready`、权限矩阵和回滚点。
5. 在独立影子目标恢复同一批次 DB/附件,验证应用 `/ready` 和关键附件引用,记录真实 RPO/RTO。
发布和远程备份已分别获得授权并完成;真实角色写入、部署和影子恢复仍须继续取得明确授权,不能由既有批准推导。
## 后续获批写入:成对异机备份
## 后续获批写入:已降级的异机备份批次
- 执行日期:2026-07-26。
- 授权范围:生成真实 DB、附件和 manifest 成对备份,并复制到用户指定的另一台主机。
- 源机与目标机设备指纹不同;未记录主机名、地址、SSH 名称、账号或完整目录。
- 完整批次:`hyb-oa-backup-20260726T133923Z-live`,包含 `database.dump`、`storage.tar.gz`、`manifest.json` 三个预期文件,总大小约 9.6 MiB。
- 源端完整性检查通过;目标端重新计算 DB/附件 SHA-256 并与 manifest 核对通过;两端 manifest 哈希一致。
- 批次 `hyb-oa-backup-20260726T133923Z-live` 的三份产物及传输校验本身完整,但后续拓扑复核证明其来源是“活动 DB + 非活动附件目录”。该批次保留作审计,不再计为活动实例的成对恢复点。
- 该批次的附件归档含 54 个文件,而活动 server 实际附件挂载为空,活动库附件类记录也均为 0;两端哈希一致只能证明传输一致,不能证明源选择正确。
- 目标批次目录权限为 `700`;传输失败产生的单文件残缺副本已精确删除,本机临时暂存数据已安全清理。
- 运行主机没有宿主 Node.js,因此为成对备份入口增加 Python 3 manifest 等价回退;无 Node 路径已通过完整合成恢复演练,本地参考 RTO 仍为 6 秒。
- 运行中的 PostgreSQL 容器参数与部署目录环境文件存在漂移;本次只在运行容器参数与活动服务数据库连接相互匹配后执行备份,未修改环境文件。
- 本次未执行恢复、部署、迁移、角色修改、企微写入或业务写入;真实格式影子恢复和恢复后 `/ready` 仍无证据。
- 必须在另行批准后,显式提供经挂载核验的 `STORAGE_DIR_HOST` 和 Compose 解析出的 DB 容器 ID,重新生成并复制正确批次。本次未执行恢复、部署、迁移、角色修改、企微写入或业务写入;真实格式影子恢复和恢复后 `/ready` 仍无证据。
## 后续获批调查:部署拓扑与企微字段可用性
- 当前 OA 公网路由由宿主反向代理进入 server 容器;后端和前端构建位于同一应用镜像,数据库位于独立容器,附件由宿主目录挂载。
- 运行容器来源目录与日常运维任务目录不同;两套目录都没有 Git 元数据,源文件相同但 Compose 与环境文件摘要不同。运行代码与本机当前代码不一致,`/ready` 无构建标识。
- 仓库部署脚本的固定宿主健康检查端口与现场实际端口不一致。直接同步并执行现有 Compose 可能产生不同容器名或第二套数据目标,故未执行部署、重启、迁移或目录改写。
- 核查时仓库部署脚本的固定宿主健康检查端口与现场实际端口不一致;本地现已改为 Compose 动态解析,并移除固定容器名、参数化数据目录,但新项目名和数据路径尚未在获批停机窗口启用,故仍未执行部署、重启、迁移或目录改写。
- 使用修正后的双 Secret 读取链路做结构化探测:成员详情 6/6 成功、部门 6/6 可读,手机号/邮箱/企业邮箱均 0/6 可读;HR 详情 6/6 返回成功但 `staff_info` 为 0/6。该结果只证明批量读取链路拿不到敏感字段。
- 企微官方文档复核:2022-06-20 后新建的自建应用不再通过 `/user/get` 返回手机、邮箱和企业邮箱;管理员需在应用详情选择敏感字段,成员使用 `snsapi_privateinfo` 手工授权,`/cgi-bin/auth/getuserinfo` 才返回 30 分钟有效的 `user_ticket`,随后 `/cgi-bin/auth/getuserdetail` 可返回成员同意的手机号、邮箱和企业邮箱。
- 当前 OA `server/src/routes/wecomAuth.js` 固定使用 `snsapi_base`,只调用 `getuserinfo` 获取 userid,不调用 `getuserdetail`;因此现状是项目尚未接入官方敏感信息授权链路,而不是企微没有该能力。