文件
hyb-oa/.trellis/tasks/08-01-production-secret-recovery-env-externalization/prd.md
T

8.4 KiB

生产密钥恢复与环境文件外置

Goal

在不把生产 Secret 写入仓库、普通备份、日志或聊天的前提下,为 OA 建立可执行的环境配置恢复合同,并把生产环境文件迁出源码同步树,避免 rsync 误删、单机损坏或单管理员缺席导致系统无法恢复。

Background

  • 5dac336 首次发布误把 Compose 配置目录当作仓库根目录,rsync --delete 删除了远端 .env;本次仅因旧 server 容器仍在运行,才能按允许键恢复配置。
  • 当前数据库和附件已有异机备份,环境文件及密钥没有独立恢复合同。主机整体丢失时,仅恢复数据库与附件不足以启动 OA。
  • 07de198 已把仓库根、远端 env、PGDATA 和 storage 的检查前移到 rsync 之前,封住最可能复发路径,但没有解决整机丢失、密钥托管或环境文件仍位于同步树内的问题。
  • 当前 .env.example 合同含 164 个键;生产事故恢复曾观察到 163 个可恢复键。第一阶段只按键名和用途分类,不读取、输出或提交生产值;第二阶段再只读解释生产键集合差异。
  • 仓库中不只有 4 个 systemd service 引用 REMOTE_ENV_FILE:Compose、部署/备份入口、自动备份安装器、日常巡检脚本、若干 QA 收口脚本及 runbook/checklist 都存在当前路径假设,迁移必须统一收敛。
  • FIELD_ENCRYPTION_KEY 已用于银行账号、身份证号和通知渠道 Secret 等数据库密文。原值丢失后不能从数据库反推;直接换新值会使既有密文不可读,除非持有旧值完成受控重加密。

Scope

  • 当前获批并执行的范围仅为 Phase 1:脱敏恢复分类、现有恢复卡、环境合同校验、聚焦测试与稳定规约。
  • Phase 2 的代码合同、生产 env 外置及旧副本删除仍保留在本任务中规划,但必须分别取得后续明确批准后才可实施。
  • 本轮不访问生产,不读取或托管 Secret,不修改 Compose/systemd,不重启服务,也不执行任何生产删除。

Requirements

Phase 1:恢复合同与分类(不碰生产)

  • 在现有 deploy/trialrun/runbook.md 的紧急恢复卡中增加环境配置恢复,不另建重复操作手册。
  • 用机器可校验的脱敏清单覆盖 .env.example 中每一个键;清单只包含键名和恢复类别,不含值、长度、哈希或原始 payload;各类别的影响和责任边界统一写入现有恢复卡。
  • 至少支持用户已确认的四类:
    1. template_rebuild:端口、路径、开关、TTL 等,从 .env.example 与 runbook 重建。
    2. external_reissue:企微、Webhook、腾讯云、SMTP、OIDC、S3/AI/ERP 等外部凭据,从对应平台重新签发或获取。
    3. direct_rotate:JWT、验证码 pepper、试运行管理员口令等可直接轮换项;必须写明会话失效、验证码失效或账号重置等代价。
    4. downtime_reset:数据库口令等不能反推明文、但可在保留 PGDATA 时通过隔离停机操作重置的项目。恢复卡必须明确这是“重置”而非“找回”,先备份认证配置、停止应用写入、隔离数据库、临时调整认证、修改角色口令、恢复认证配置并做完整 smoke;不得让数据库临时暴露公网。
  • 第 5 类 preserve_original 为最高风险:FIELD_ENCRYPTION_KEY 等必须保存原值的密钥,只能从受控共享密码库/加密封存副本恢复;没有旧值时不得宣称轮换即可恢复,必须如实说明既有银行账号、身份证号和通知渠道密文将不可读并需重新收集/配置。
  • 密钥封存不得只由单一管理员持有。共享密码库或加密封存副本至少由系统管理员和公司负责人各自可取;仓库只记录责任角色、复核周期和恢复验证,不记录产品库地址、账号、恢复码或密钥正文。
  • 第一阶段限制为文档、脱敏分类清单、静态校验和测试,目标半天内完成;不连接生产、不创建密码库条目、不迁移 env、不重启服务。

Phase 2:环境文件外置(另行生产批准)

  • 生产 REMOTE_ENV_FILE 迁移到源码同步树外的 root-only 绝对路径,目标权限为 root 所有、0600、普通非 symlink 文件。
  • Compose 的 server、worker、init、minio_init,部署入口、备份/巡检脚本和 4 个运维 service 必须统一使用显式 REMOTE_ENV_FILE,不再依赖 ./.env。
  • deploy.sh 对 existing 生产部署拒绝 REMOTE_ENV_FILE 与 REMOTE_DIR 相同或互相嵌套;validate-env-contract.js 负向断言生产 Compose/部署合同不得把 env 放回同步树。不能仅依赖 rsync exclude。
  • 迁移使用受控短暂停机/切换窗口:从当前事实源原子复制到外置路径,验证 owner/mode/非 symlink 与键集合,更新全部 units,完成 Compose config、新 init 配置预检、应用切换、/ready、外部依赖、runtime smoke、登录/企微和备份 timer 验证。
  • 迁移期允许存在新旧两份密钥副本作为明确回退点;验收后必须按精确旧路径删除旧副本,并再次验证旧文件不存在、运行容器与 systemd 均引用新路径。删除属于生产破坏性动作,必须在第二阶段批准窗口内显式执行和记录。
  • 共享密码库具体产品、成员授权和生产 Secret 写入由用户/公司负责人在仓库外完成;Codex 只验证职责与完成状态,不读取 Secret。

Acceptance Criteria

  • .env.example 当前全部键恰好归入一个获批恢复类别;新增、删除或重复分类会使合同测试失败。
  • 恢复卡能分别回答模板重建、外部重申请、直接轮换、数据库停机重置和原值保留;每类写明业务影响、操作边界和恢复后验证,preserve_original 标为最高风险。
  • 数据库口令恢复明确使用隔离停机“重置”流程,不声称可从 pg_authid 反推明文,不留下 trust 认证或公网暴露。
  • 密钥托管职责至少覆盖系统管理员与公司负责人两人;仓库、日志和普通数据库/附件备份中不出现密钥正文。
  • Phase 1 的文档、分类清单、validator 和单元测试通过,且没有生产访问或写入。
  • Phase 2 代码合同使生产 env 物理位于同步树外,并负向拒绝同步树内 env;所有 Compose/部署/备份/巡检引用统一。
  • 获批生产迁移后,新路径 owner/mode、Compose config、init 配置预检、/ready、外部依赖、runtime smoke、登录/企微及 4 个 timer 均通过。
  • 旧副本删除后有否定验证,生产运行合同仍指向新路径;可回退点、删除时间和操作人有脱敏审计记录。

Out of Scope

  • 不把明文 .env 加入现有数据库/附件备份或 Git。
  • 不在本任务内采购或替用户选择密码管理器产品。
  • 不让 Codex、CI、聊天或任务证据读取/保存生产 Secret。
  • 不借机轮换所有密钥、重加密全部敏感字段或重构配置中心。
  • 不在本任务实现密钥版本、keyring、惰性重加密或全量重加密;另立安全设计任务后才能改变密文格式。
  • 不在 Phase 1 迁移生产路径、更新 systemd、重启容器或删除旧 env。

Known Limitation / Deferred

  • 当前 AES-256-GCM 密文格式为 v1:<iv>:<tag>:<ciphertext>,只有格式版本,没有 key id;服务端配置也只有单一 FIELD_ENCRYPTION_KEY,因此无法判断密文由哪把钥匙生成或安全并存新旧钥匙。
  • preserve_original 是当前恢复现实,不是永久设计。若将来怀疑密钥泄露,现有系统无法在保持既有密文可读的同时轮换。
  • 后续独立任务应设计带 key id 的新密文版本、受控 keyring、写入只用当前钥匙、旧密文读取后的惰性重加密,以及覆盖全部密文字段的可审计批量迁移/回滚;在该设计完成前不得删除旧钥匙。

Test Plan

  • 环境与语法:node deploy/trialrun/validate-env-contract.js、相关 node --check、cd server && npm run prisma:validate。
  • 回归测试:聚焦运行恢复分类与部署安全单元测试,并运行 cd server && npm test 全量测试。
  • Trellis/文档门禁:task validate、Trellis governance、process contract、docs registry audit、sensitive-content scan 与 git diff --check。
  • 人工复核:逐项检查 164 个键的恢复语义、恢复卡的停机/隔离边界,以及新增材料不含生产值、哈希或真实托管位置。