# 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 marker；backup-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 2：Google 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 5：Credential 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 6：Full 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 7：Full 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/path，mode `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
