Files
awoooi/docs/runbooks/OFFSITE-BACKUP-ESCROW-RUNBOOK.md
Your Name cfb866d055
Some checks failed
Ansible Lint / lint (push) Successful in 35s
CD Pipeline / tests (push) Failing after 13s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
Code Review / ai-code-review (push) Failing after 11s
feat(governance): add agent market automation surfaces
2026-06-04 21:50:55 +08:00

430 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Offsite Backup / Credential Escrow 操作手冊
> 版本2026-05-19.v4
> 適用範圍110 備份中心、Google Drive/rclone 離機備份、credential escrow 覆核 marker
---
## 目標
這份手冊用來把「本地備份已完成」推進到「整台 110 遺失時仍可恢復」。
它處理兩個缺口:
1. 離機備份13 個本地 restic repo 必須至少有一份可到達 110 以外的位置。
2. 憑證金庫restic password、Google Drive rclone.conf/OAuth、break-glass admin、DNS/registrar/OAuth recovery 必須在密碼管理器或離線加密金庫可找到、可解密、可用。
本手冊不保存任何 secret。所有指令都不得把密碼、token、recovery code、private key 貼到 shell transcript、LOGBOOK、Telegram、Prometheus label 或 repo。
---
## 絕對禁止
- 禁止把 Google Drive OAuth token、rclone config、restic password、OAuth recovery code 寫進 git。
- 禁止把 secret 當成 `evidence-id``note` 傳給 `mark-credential-escrow-verified.sh`
- 禁止在 Google Drive/rclone 未配置或 gate blocked 時跑 full sync。
- 禁止由子備份腳本或臨時手動指令刪除遠端備份。唯一例外是 `/backup/scripts/sync-offsite-backups.sh --mode sync`,它在 full/partial gate 通過後用 `OFFSITE_SYNC_DELETE_OLD=1` 鏡像本地 latest-only restic repo刪除 Google Drive 上已不屬於最新 repo 狀態的舊檔。
- 禁止把 restore 直接套到 production DB、production namespace 或正式 volume。
- 禁止為了清告警假造 escrow marker。marker 只能在人工確認金庫項目可用後建立。
---
## 狀態判讀
| 狀態 | 意義 | 下一步 |
|------|------|--------|
| `READY_WITH_WARNINGS` | 本地 repo 可檢查,但 Google Drive/rclone 或 escrow 還沒完成 | 可以繼續設定 Google Drive/rclone / 金庫,不可 full sync |
| `BLOCKED` | 必要條件缺失,例如 rclone remote 未配置卻要求 dry-run/full sync | 先修 blocked 項目 |
| `READY` | Google Drive/rclone、small repo、marker、金庫覆核都符合 gate | 可排小範圍 sync 或 full sync review |
Prometheus 裡的 `BackupOffsiteCopyNotConfigured``BackupCredentialEscrowEvidenceMissing` 是恢復能力缺口不代表網站立即故障但如果長期存在代表「災難時可能無法復原」。repo 工作站可用 live visibility check 確認缺口告警真的進入 Prometheus / Alertmanager
```bash
python3 scripts/ops/backup-alert-live-visibility-check.py --prometheus-url http://192.168.0.110:9090 --alertmanager-url http://192.168.0.110:9093
```
這支檢查只讀 API不送測試告警、不改 route、不改 silence。它會在缺口 metric 存在時要求告警 firing/active如果 Google Drive/rclone 或 escrow 已補齊,對應告警不需要繼續 firing。
備份保留策略固定為 latest-only本地 restic repo 在新 snapshot 成功後執行 `--group-by "" --keep-last 1 --prune`188 MOMO PostgreSQL 檔案備份在新檔成功後只留最新一份Google Drive/rclone full sync 以本地 repo 為準鏡像,成功後刪除遠端舊檔,且 `RCLONE_DRIVE_USE_TRASH=false`,避免舊備份只進 Google Drive 垃圾桶。Prometheus 指標 `awoooi_backup_retention_latest_only``awoooi_backup_retention_offsite_delete_old_enabled` 必須為 `1`,且每個 110 restic repo 的 `awoooi_backup_job_snapshot_count` 必須小於等於 1否則 retention 告警會進 Telegram。
---
## Phase 0確認本地備份綠燈
在 110 上執行:
```bash
/backup/scripts/offsite-escrow-evidence-report.sh --no-color
/backup/scripts/backup-offsite-readiness-gate.sh --status --no-color
grep -E 'awoooi_backup_last_run_failed_count|awoooi_backup_job_fresh|awoooi_backup_integrity_fresh' /home/wooo/node_exporter_textfiles/backup_health.prom
```
在 repo 工作站執行:
```bash
SSH_BATCH_MODE=yes bash scripts/reboot-recovery/full-stack-cold-start-check.sh --monitor-read-only --no-color --watch --interval 1 --max-attempts 1
SSH_BATCH_MODE=yes bash scripts/reboot-recovery/p3-controlled-release-gate.sh --no-color
```
成功條件:
- `awoooi_backup_last_run_failed_count{exported_job="backup_all"} = 0`
- 110 有 13 個 `awoooi_backup_job_fresh`
- restic check / restore drill fresh
- cold-start gate 沒有 blocked
- `offsite-escrow-evidence-report.sh` 會輸出目前 `NEXT_STEP`,且不含任何 credential 值
---
## Phase 0.5:產出可交接 evidence report
每次 Google Drive/rclone 設定、small dry-run、partial sync、escrow 覆核、full sync 前後都先產出一份紅acted report。這份 report 可以貼到 LOGBOOK 或交接訊息,但仍要先目視確認沒有 secret。
110 每日 06:15 也會自動產生同一份 report 到 `/backup/logs/offsite-escrow-evidence-report.log`。這條 cron 只做本機只讀判讀,不會查 remote、不會上傳、不會寫 success markerbackup-health exporter 會把 cron 是否存在納入 `awoooi_backup_job_configured`
```bash
/backup/scripts/offsite-escrow-evidence-report.sh --no-color
```
如果已經設定 Google Drive/rclone且需要確認 remote 可列出,才加:
```bash
/backup/scripts/offsite-escrow-evidence-report.sh --include-remote-status --no-color
```
`--include-remote-status` 只會跑 `sync-offsite-backups.sh --mode status`,不會上傳、不會寫 success marker但它會查 remote因此只在 Google Drive/rclone 已設定後使用。
在 repo 工作站也可以產生全站收斂 scorecard
```bash
bash scripts/reboot-recovery/full-stack-recovery-scorecard.sh
```
若要把目前 DR 缺口直接轉成 operator 可照做的下一步命令,使用只讀 checklist
```bash
bash scripts/reboot-recovery/dr-offsite-operator-checklist.sh --no-color
```
這支 checklist 會彙整 repo scorecard、Prometheus recording rule、110 紅acted evidence report並依 `NEXT_STEP` 印出下一段應在 110 TTY 執行的命令。它不會查詢或輸出 secret、不會上傳資料、不會寫 provider / escrow / sync marker真正的寫入與同步仍必須由 operator 在 110 本機明確執行。
同一個 next-step 也會進入 110 textfile metric讓 AI 巡檢不用解析人工 log
```promql
awoooi_backup_dr_next_step_info{host="110"}
awoooi_backup_offsite_partial_fresh{host="110",provider="rclone"}
awoooi_backup_dr_credential_escrow_missing_count{host="110"}
```
這些 metric 只描述階段與缺口,不包含 Google Drive token、restic password 或 evidence-id。
若輸出 `RECOVERY_STATE=CORE_READY_DR_OFFSITE_PENDING`,代表網站與 cold-start gate 已恢復,但本手冊的 Google Drive/rclone / escrow / full offsite marker 還沒完成。此狀態不可當成 DR 完成,只能當成核心服務恢復完成。
要防止人為誤判,使用嚴格 gate
```bash
bash scripts/reboot-recovery/full-stack-recovery-scorecard.sh --require-dr
```
人工完成 5 個 credential escrow marker 後,用最終 gate 做收斂判定。這條命令會同時檢查 repo scorecard、110 Prometheus recovery recording rule、備份告警可見性與 110 紅acted evidence report任何一層不同步都會失敗。
```bash
bash scripts/reboot-recovery/dr-offsite-operator-checklist.sh --require-dr
```
如果 marker 剛寫完Prometheus scrape、recording rule 與 Alertmanager 可能需要幾分鐘才會同步。這時不要手動猜狀態,也不要重複亂改 marker在 repo 工作站執行 post-marker 等待器,讓它只讀輪詢到四層 gate 一致:
```bash
bash scripts/reboot-recovery/wait-dr-offsite-ready.sh --timeout-seconds 900 --interval-seconds 30 --no-color
```
這支腳本只讀 `full-stack-recovery-scorecard.sh --require-dr``recovery-scorecard-contract-check.py --expect-dr-ready``backup-alert-live-visibility-check.py``dr-offsite-operator-checklist.sh --require-dr`。它不會建立 escrow marker、不會上傳或刪除備份、不會列印 credential若 timeout 時仍顯示 `ESCROW_MISSING_COUNT>0`,代表人工作業尚未完成,不可偽造 marker。
`OFFSITE_CONFIGURED=0``ESCROW_MISSING_COUNT>0``FULL_MARKER_PRESENT=0` 時,這條指令必須失敗;這是預期行為,不可用 fake marker 清掉。
Prometheus 最終合約也必須同步驗證:
```bash
python3 scripts/ops/recovery-scorecard-contract-check.py --prometheus-url http://192.168.0.110:9090 --expect-core-ready --expect-dr-ready
```
在 full sync / escrow 還沒完成前,`--expect-dr-ready` 必須失敗;完成後才應通過。
Prometheus 也會用 `awoooi_recovery_dr_offsite_ready{host="110"}` 呈現同一個 DR gate。此值目前應為 `0`;只有 Phase 7 full sync 完成且 Phase 5 escrow marker 全部 fresh 後,才應變為 `1`
判讀重點:
| `NEXT_STEP` | 意義 |
|-------------|------|
| `configure_google_drive_rclone_on_110_tty` | 還沒設定 Google Drive/rclone回 Phase 1 |
| `run_small_dry_run_then_partial_sync` | rclone remote 已配置,尚未證明小範圍 offsite sync |
| `complete_credential_escrow_review` | offsite 小範圍已證明,還缺金庫覆核 marker |
| `pre_full_sync_review` | 可安排低峰 full sync 前檢查 |
| `offsite_and_escrow_ready` | 離機備份與金庫證據皆已到位 |
---
## Phase 1在 110 本機設定 Google Drive/rclone
優先使用互動模式。不要把 Google Drive OAuth token 或 rclone.conf 貼到聊天或文件。
```bash
ssh wooo@192.168.0.110
/backup/scripts/configure-offsite-rclone.sh --interactive
/backup/scripts/configure-offsite-rclone.sh --status
```
> `configure-offsite-b2.sh` 是 legacy 相容工具;目前預設用 Google Drive/rclone不需要 `B2_ACCOUNT_ID`。
### Phase 1.5:建立 Google Drive root-scoped remote
Google Drive 帳號若檔案很多,`gdrive:awoooi-backups/restic/...` 可能每次都花數分鐘解析資料夾路徑。OAuth 完成後,建立一個只指向 `awoooi-backups/restic` 的 root-scoped remote後續備份使用 `gdrive_awoooi_restic:`,避免 full sync 被 Drive 根目錄查找拖慢。
```bash
OFFSITE_RCLONE_SOURCE_REMOTE=gdrive \
OFFSITE_RCLONE_ROOT_REMOTE=gdrive_awoooi_restic \
OFFSITE_RCLONE_ROOT_PATH=awoooi-backups/restic \
/backup/scripts/configure-offsite-rclone.sh --create-root-remote
/backup/scripts/configure-offsite-rclone.sh --status
```
成功條件:
```text
ROOT_SCOPED_REMOTE_READY=gdrive_awoooi_restic:
OFFSITE_RCLONE_REMOTE=gdrive_awoooi_restic
OFFSITE_REMOTE_ROOT=gdrive_awoooi_restic:
RCLONE_REMOTE_CONFIGURED=1
```
這個步驟會複用既有 `gdrive` remote 的 OAuth token並在 host-local `rclone.conf` 寫入 `root_folder_id`;不會把 token 寫進 `/backup/scripts/offsite.env`、repo、LOGBOOK 或 Telegram。
成功條件:
```text
RCLONE_PRESENT=1
OFFSITE_PROVIDER=rclone
OFFSITE_RCLONE_REMOTE=gdrive
RCLONE_REMOTE_CONFIGURED=1
OFFSITE_ENV_PRESENT=1
OFFSITE_ENV_MODE_OK=1
```
如果必須用環境變數寫入,只能在受控 shell 中操作,並確認 shell history 不會保存 secret。完成後立刻檢查檔案權限
```bash
ls -l /backup/scripts/offsite.env
/backup/scripts/configure-offsite-rclone.sh --status
```
---
## Phase 2Google Drive/rclone 設定後跑 readiness gate
```bash
/backup/scripts/backup-offsite-readiness-gate.sh --status --require-configured --no-color
```
成功條件:
- 沒有 `BLOCKED`
- rclone remote 已配置,例如 `gdrive:`
- rclone command 存在
- `ai-artifacts``public-routes` 本地 repo 存在
如果只有 escrow marker warning可以繼續做 rclone dry-run但仍需在 full sync 前完成金庫覆核。
---
## Phase 3小範圍 dry-run
先只測很小的 repo不碰 87G 全量資料。
```bash
/backup/scripts/backup-offsite-readiness-gate.sh --dry-run-small --no-color
```
這會對 `ai-artifacts public-routes` 跑 rclone dry-run。成功後再執行明確的小範圍 dry-run
```bash
/backup/scripts/sync-offsite-backups.sh --mode dry-run --repos "ai-artifacts public-routes"
```
成功條件:
- rclone dry-run 完成
- 沒有 authentication error
- 沒有 remote/path permission error
- 沒有本地 repo 缺失
安全護欄:
- `sync-offsite-backups.sh --mode sync` 預設會先檢查 1 分鐘 load不得高於 `OFFSITE_SYNC_MAX_LOAD_1=12`
- `/backup` 使用率不得高於 `OFFSITE_SYNC_MAX_BACKUP_DISK_USED_PCT=92`
- full 13 repo sync 不得與本地備份程序重疊,且必須距離下一次備份排程至少 `OFFSITE_SYNC_FULL_MIN_RUNWAY_MINUTES=270` 分鐘;手動執行時若接近 08:00/14:00/20:00 AWOOOI 高頻備份gate 會 BLOCKED應等待 03:00 gated cron 或下一個低峰窗口。
- 成功通知預設不送 Telegram證據留在 log、textfile、Prometheus。失敗仍會告警。
---
## Phase 4小範圍 partial sync
小 repo dry-run 成功後,才做 partial sync
```bash
/backup/scripts/sync-offsite-backups.sh --mode sync --repos "ai-artifacts public-routes"
/backup/scripts/backup-offsite-readiness-gate.sh --status --require-configured --no-color
```
預期結果:
- 寫入 `/backup/offsite/rclone-partial-last-success`
- 寫入 per-repo marker
- 不會寫 `/backup/offsite/rclone-last-success`
full success marker 只能在 13 repo 全部同步成功後建立,避免 partial sync 誤清 full offsite stale。
---
## Phase 5Credential escrow 覆核
人工確認密碼管理器或離線加密金庫後,才寫 marker。marker 只能放證據 ID不放 secret。
先看缺口:
```bash
/backup/scripts/mark-credential-escrow-verified.sh --status
/backup/scripts/mark-credential-escrow-verified.sh --missing-commands
```
逐項覆核後寫入 marker建議直接使用 `--missing-commands` 印出的缺失項目模板,只替換 `EVIDENCE_ID_FOR_*`。直接使用 placeholder 會被拒絕;正式寫入前可先加 `--dry-run` 驗證 evidence-id不會建立 marker
```bash
/backup/scripts/mark-credential-escrow-verified.sh --missing-commands
# 將輸出的 EVIDENCE_ID_FOR_* 換成不含 secret 的證據 ID 後,可先加 --dry-run 驗證其中一條。
```
正式寫入 marker 後,腳本會嘗試立即刷新 110 的 `backup_health.prom`,讓 `awoooi_backup_credential_escrow_fresh``awoooi_backup_dr_credential_escrow_missing_count` 與 Prometheus 告警更快收斂;如果 exporter 暫時不可用marker 仍會保留,下一輪 cron 會補刷新。輸出應包含 `MARKER_WRITTEN`,且在 exporter 可用時包含 `TEXTFILE_REFRESHED`
可接受的 `evidence-id`
- 密碼管理器項目 ID
- 工單 ID
- sealed envelope ID
- recovery checklist ID
不可接受的 `evidence-id`
- 密碼、token、recovery code、secret URL
- private key、OAuth token、rclone.conf 內容
- 任何可直接登入或還原的秘密值
- `EVIDENCE_ID_FOR_*``VAULT-ITEM-ID``TODO``CHANGE_ME` 等 placeholder
不可接受:
- 密碼、token、API key
- recovery code
- private key
- 含 secret 的 URL
---
## Phase 6Full sync 前檢查
全 13 repo 約 87G。只能在低峰窗口與 operator review 後執行。
先跑不會上傳的 full sync 前檢查:
```bash
/backup/scripts/backup-offsite-readiness-gate.sh --pre-full-sync --require-configured --require-escrow --no-color
```
成功條件:
- 13 個本地 repo 都存在
- Google Drive/rclone 配置完整
- escrow marker 都 fresh
- 110 host load 低於 gate
- 沒有正在執行的本地備份程序,且距離下一次備份排程有足夠 runway
- P3 gate 沒有 blocked
若手動 full sync 已經開始,但實測速度顯示大型 repo 會撞到 `02:00` / `08:00` / `14:00` / `20:00` 備份窗口,優先保護本地備份。做法是停止目前的 `sync-offsite-backups.sh --mode sync` 與其 rclone child清掉 `/tmp/awoooi-offsite-backup.lock`,並寫入 `/backup/offsite/rclone-manual-protective-stop.status`,至少包含 `status``timestamp``completed_or_verified_repos``remaining_repos``next_step`。不得手寫 `/backup/offsite/rclone-last-success`full marker 只能由完整 13 repo sync 成功後自動產生。
再確認容量:
```bash
du -sh /backup/awoooi /backup/configs /backup/gitea /backup/harbor /backup/momo /backup/langfuse /backup/monitoring /backup/signoz /backup/open-webui /backup/clawbot /backup/sentry /backup/ai-artifacts /backup/public-routes
```
---
## Phase 7Full sync
只有 Phase 6 全綠、確認低峰窗口、且人工明確啟用 full sync marker 後才執行:
```bash
install -d -m 750 /backup/offsite
touch /backup/offsite/enable-rclone-sync
/backup/scripts/sync-offsite-backups.sh --mode sync
```
`enable-rclone-sync` 是第二層保險,避免有人或 cron 在未審核時直接啟動 13 repo 全量同步。若要臨時只做人工 full sync 而不啟用每日 03:00 gated cron必須改用受控環境變數
```bash
OFFSITE_SYNC_REQUIRE_ENABLE_MARKER_FOR_FULL=0 /backup/scripts/sync-offsite-backups.sh --mode sync
```
除非當下有人盯著負載與 log否則不要用這個覆寫。
完成後驗證:
```bash
/backup/scripts/offsite-escrow-evidence-report.sh --include-remote-status --no-color
/backup/scripts/verify-offsite-full-sync.sh --write-textfile --no-color
/backup/scripts/backup-offsite-readiness-gate.sh --status --require-configured --require-escrow --no-color
grep -E 'awoooi_backup_offsite_|awoooi_backup_credential_escrow_' /home/wooo/node_exporter_textfiles/backup_health.prom
grep -E 'awoooi_backup_offsite_remote_|awoooi_backup_offsite_full_verify_' /home/wooo/node_exporter_textfiles/offsite_full_sync_verify.prom
```
預期:
- `/backup/offsite/rclone-last-success` 存在且 fresh
- `awoooi_backup_offsite_fresh{provider="rclone"} = 1`
- `awoooi_backup_offsite_remote_verify_ok{provider="rclone"} = 1`
- 13 個 `awoooi_backup_offsite_remote_snapshot_count{provider="rclone"}` 都等於 `1`
- `BackupOffsiteCopyNotConfigured` 解除
- `BackupOffsiteCopyStale` 不 firing
- `BackupOffsiteFullVerifyFailed` 不 firing
- `BackupOffsiteRemoteSnapshotRetentionExceeded` 不 firing
- escrow 五項 fresh 後,`BackupCredentialEscrowEvidenceMissing` 解除
---
## 故障處理
| 症狀 | 判讀 | 處理 |
|------|------|------|
| `Google Drive/rclone remote not configured` | 110 尚未完成 rclone Google Drive OAuth 或 remote 名稱不符 | 回 Phase 1 |
| 小 repo 只有數 MB 但 `rclone copy` 花數分鐘 | Drive 根目錄路徑解析過慢 | 執行 Phase 1.5,改用 `gdrive_awoooi_restic:` |
| `rclone 未安裝` | host package 缺失 | 先由 Ansible/ops 安裝 rclone再重跑 gate |
| `directory not found` 或 permission denied | Google Drive remote/path 權限不符 | 修 rclone remote 或 Drive folder 權限,不要改 repo |
| small dry-run 成功但 full pre-check blocked | 13 repo 或 escrow 不完整 | 先修 blocked 項目 |
| full sync 中 host load 過高 | 同步窗口不合適 | 中止後改低峰窗口;不要降低資料庫/ClickHouse memory 來硬跑 |
| Prometheus 還在 pending | alert 有 `for` 時間或 exporter 未刷新 | 先刷新 exporter再查 `/api/v1/alerts` |
---
## 完成定義
離機備份與金庫不能只靠一次手動成功。真正完成需滿足:
- Google Drive/rclone remote 存在於 110 host-local `rclone.conf``offsite.env` 只保存非 secret remote/pathmode `0600`
- small dry-run 成功
- small partial sync 成功
- full sync 在低峰窗口成功
- full sync 後 `verify-offsite-full-sync.sh --write-textfile` 成功,並證明 Google Drive 13 個 repo 皆只保留 1 份 snapshot
- full offsite marker fresh
- 五個 credential escrow marker fresh
- Prometheus offsite / escrow warning 清除
- LOGBOOK 記錄 snapshot / marker / gate 證據,但不含任何 secret