# ADR-012: 危險操作治理架構 **狀態**: 提案 **日期**: 2026-03-23 **決策者**: 統帥 **觸發事件**: Telegram Token 連續三次被 logOut 災難 --- ## 背景 ### 事故摘要 2026-03-23,AWOOOI API 程式碼中的 `logOut` API 呼叫導致 Telegram Bot Token 連續三次被永久失效: ``` 時間線: 11:09 - 第一個 Token 被 logOut (舊版 AWOOOI API) 19:31 - 第二個 Token 被 logOut (舊 Pod 未完全終止) 19:39 - 第三個 Token 成功 (AWOOOI API 已停用 Telegram) ``` ### 問題根因 | 問題 | 說明 | |------|------| | 危險程式碼進入生產 | `logOut` 永久失效 Token,無人攔截 | | 無 Code Review | 涉及外部服務的危險操作未被審查 | | 無執行時告警 | 危險操作執行時沒有任何通知 | | 無審計日誌 | 無法追溯誰/何時執行了什麼 | | 變更順序錯誤 | 給新 Token 時舊服務還在運行 | --- ## 決策 建立**三層危險操作治理架構**: 1. **預防層** - CI/CD 攔截危險模式 2. **執行層** - 危險操作需人工確認 3. **審計層** - 所有操作記錄可追溯 --- ## 架構設計 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ 危險操作治理架構 (Dangerous Operations Governance) │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Layer 1: 預防層 (Prevention) │ │ │ │ ════════════════════════════ │ │ │ │ │ │ │ │ CI/CD Pipeline │ │ │ │ ├── 危險模式掃描 (grep logOut, deleteWebhook, etc.) │ │ │ │ ├── 外部服務 API 白名單檢查 │ │ │ │ └── PR 標記 (涉及危險操作需額外審核) │ │ │ │ │ │ │ │ Code Review │ │ │ │ ├── CODEOWNERS: 危險檔案需 CTO/CISO 審核 │ │ │ │ └── PR Template: 勾選「是否涉及外部服務」 │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Layer 2: 執行層 (Execution Control) │ │ │ │ ═══════════════════════════════════ │ │ │ │ │ │ │ │ 危險操作分級 │ │ │ │ ├── Tier 0 (禁止): logOut, revokeToken │ │ │ │ ├── Tier 1 (需審批): deleteWebhook, setWebhook │ │ │ │ ├── Tier 2 (需告警): sendMessage (批量) │ │ │ │ └── Tier 3 (自動): getMe, getUpdates │ │ │ │ │ │ │ │ 執行前檢查 │ │ │ │ ├── 產生簽核卡片 (Tier 1 操作) │ │ │ │ ├── 發送 Telegram 告警 (Tier 1-2 操作) │ │ │ │ └── 記錄到活躍事件 │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Layer 3: 審計層 (Audit Trail) │ │ │ │ ═════════════════════════════ │ │ │ │ │ │ │ │ 記錄內容 │ │ │ │ ├── 操作類型、時間、執行者 │ │ │ │ ├── 影響範圍 (哪些服務/Token) │ │ │ │ ├── 執行結果 (成功/失敗) │ │ │ │ └── 關聯的 Incident ID │ │ │ │ │ │ │ │ 儲存位置 │ │ │ │ ├── PostgreSQL: audit_logs 表 │ │ │ │ ├── SignOz: Trace 追蹤 │ │ │ │ └── 活躍事件: 顯示在 Dashboard │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## 危險 API 清單 ### Telegram API | API | 分級 | 說明 | 處理方式 | |-----|------|------|----------| | `logOut` | Tier 0 (禁止) | 永久失效 Token | CI/CD 攔截,禁止進入生產 | | `close` | Tier 0 (禁止) | 關閉所有 session | CI/CD 攔截 | | `deleteWebhook` | Tier 1 (審批) | 可能影響其他服務 | 需簽核卡片 | | `setWebhook` | Tier 1 (審批) | 可能覆蓋其他設定 | 需簽核卡片 | | `sendMessage` (批量) | Tier 2 (告警) | 可能 spam 用戶 | 發送告警 | | `getUpdates` | Tier 3 (自動) | 只能單一實例 | 限定 OpenClaw | | `getMe` | Tier 3 (自動) | 無副作用 | 允許 | ### K8s API | 操作 | 分級 | 說明 | 處理方式 | |------|------|------|----------| | `kubectl delete namespace` | Tier 0 (禁止) | 刪除整個 namespace | 絕對禁止 | | `kubectl delete pvc` | Tier 1 (審批) | 刪除持久化資料 | 需簽核 | | `kubectl rollout restart` | Tier 2 (告警) | 重啟服務 | 發送告警 | | `kubectl scale` | Tier 2 (告警) | 調整副本數 | 發送告警 | | `kubectl get` | Tier 3 (自動) | 只讀操作 | 允許 | ### Database | 操作 | 分級 | 說明 | 處理方式 | |------|------|------|----------| | `DROP DATABASE` | Tier 0 (禁止) | 刪除資料庫 | 絕對禁止 | | `TRUNCATE TABLE` | Tier 1 (審批) | 清空表格 | 需簽核 | | `DELETE FROM` (無 WHERE) | Tier 1 (審批) | 刪除所有資料 | 需簽核 | | `ALTER TYPE` | Tier 2 (告警) | 修改 schema | 發送告警 | --- ## 實施計畫 ### Phase 1: CI/CD 攔截 (Week 1) ```yaml # .github/workflows/ci.yml - name: Scan for Dangerous Operations run: | echo "🔍 掃描危險操作..." # Tier 0: 禁止的操作 BANNED_PATTERNS="logOut|\.close\(\)|revokeToken|DROP DATABASE|TRUNCATE TABLE" if grep -rn --include="*.py" -E "$BANNED_PATTERNS" apps/; then echo "❌ 發現 Tier 0 禁止操作!" echo "請移除以下危險程式碼後重新提交" exit 1 fi # Tier 1: 需要審批的操作 REVIEW_PATTERNS="deleteWebhook|setWebhook|DELETE FROM" if grep -rn --include="*.py" -E "$REVIEW_PATTERNS" apps/; then echo "⚠️ 發現 Tier 1 操作,需要 CTO/CISO 審核" echo "::warning::此 PR 包含危險操作,請確認已通過審核" fi echo "✅ 危險操作掃描完成" ``` ### Phase 2: CODEOWNERS (Week 1) ``` # .github/CODEOWNERS # 涉及外部服務的檔案需要額外審核 apps/api/src/services/telegram_*.py @awoooi/cto @awoooi/ciso apps/api/src/services/executor.py @awoooi/cto @awoooi/ciso k8s/**/02-network-policy.yaml @awoooi/cio @awoooi/sre # 危險操作相關 apps/api/src/services/*gateway*.py @awoooi/cto ``` ### Phase 3: 執行時控制 (Week 2) ```python # apps/api/src/core/dangerous_ops.py from enum import Enum from typing import Callable, Any import structlog logger = structlog.get_logger() class OperationTier(Enum): FORBIDDEN = 0 # 絕對禁止 REQUIRES_APPROVAL = 1 # 需要簽核 REQUIRES_ALERT = 2 # 需要告警 AUTOMATIC = 3 # 自動允許 DANGEROUS_OPERATIONS = { "telegram.logOut": OperationTier.FORBIDDEN, "telegram.close": OperationTier.FORBIDDEN, "telegram.deleteWebhook": OperationTier.REQUIRES_APPROVAL, "telegram.setWebhook": OperationTier.REQUIRES_APPROVAL, "k8s.deleteNamespace": OperationTier.FORBIDDEN, "k8s.deletePVC": OperationTier.REQUIRES_APPROVAL, "k8s.rolloutRestart": OperationTier.REQUIRES_ALERT, } async def execute_dangerous_operation( operation_name: str, operation_fn: Callable, *args, **kwargs ) -> Any: """執行危險操作的統一入口""" tier = DANGEROUS_OPERATIONS.get(operation_name, OperationTier.AUTOMATIC) if tier == OperationTier.FORBIDDEN: logger.error( "forbidden_operation_blocked", operation=operation_name, ) raise PermissionError(f"操作 {operation_name} 被禁止執行") if tier == OperationTier.REQUIRES_APPROVAL: # 產生簽核卡片,等待 Y/n approval = await create_approval_request( title=f"危險操作: {operation_name}", description=f"即將執行 {operation_name},需要人工確認", tier="Tier 1", ) if not approval.approved: raise PermissionError("操作被拒絕") if tier in [OperationTier.REQUIRES_APPROVAL, OperationTier.REQUIRES_ALERT]: # 發送告警 await send_telegram_alert( f"⚠️ 危險操作執行中: {operation_name}" ) # 記錄審計日誌 await log_audit_trail( operation=operation_name, tier=tier.name, args=str(args), kwargs=str(kwargs), ) # 執行操作 result = await operation_fn(*args, **kwargs) # 記錄結果 await log_audit_trail( operation=operation_name, result="success", ) return result ``` ### Phase 4: 審計日誌 (Week 2) ```sql -- 新增審計日誌表 CREATE TABLE IF NOT EXISTS audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), operation VARCHAR(255) NOT NULL, tier VARCHAR(50) NOT NULL, actor VARCHAR(255), -- 執行者 (系統/用戶) target VARCHAR(255), -- 影響目標 args JSONB, result VARCHAR(50), -- success/failure/blocked error_message TEXT, incident_id VARCHAR(50), -- 關聯的 Incident trace_id VARCHAR(50), -- SignOz Trace ID created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_audit_logs_operation ON audit_logs(operation); CREATE INDEX idx_audit_logs_timestamp ON audit_logs(timestamp); CREATE INDEX idx_audit_logs_tier ON audit_logs(tier); ``` --- ## 變更管理流程 ### Token/Secret 更新 SOP ``` ┌─────────────────────────────────────────────────────────────────┐ │ Token/Secret 更新 SOP │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Step 1: 停止所有使用該 Token 的服務 │ │ ──────────────────────────────────── │ │ - kubectl scale deployment --replicas=0 │ │ - docker stop │ │ - 等待所有實例完全終止 │ │ │ │ Step 2: 驗證沒有殘留實例 │ │ ──────────────────────────── │ │ - kubectl get pods (確認沒有 Running/Terminating) │ │ - docker ps (確認沒有相關容器) │ │ │ │ Step 3: 取得新 Token │ │ ──────────────────────── │ │ - @BotFather → Revoke current token │ │ - 複製新 Token │ │ │ │ Step 4: 更新到唯一的服務 │ │ ──────────────────────── │ │ - 更新 .env 或 K8s Secret │ │ - 記錄到 memory/reference_telegram_token.md │ │ │ │ Step 5: 啟動服務並驗證 │ │ ──────────────────────── │ │ - 啟動服務 │ │ - 檢查日誌確認無錯誤 │ │ - 測試 Telegram 連線 │ │ │ │ Step 6: 更新文檔 │ │ ──────────────── │ │ - 更新 memory 相關 MD │ │ - 發送 Telegram 確認訊息 │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 驗收標準 | 項目 | 狀態 | |------|------| | CI/CD 危險模式掃描 | ⬜ | | CODEOWNERS 危險檔案審核 | ⬜ | | Tier 0 操作絕對禁止 | ⬜ | | Tier 1 操作需簽核卡片 | ⬜ | | Tier 2 操作發送告警 | ⬜ | | 審計日誌表建立 | ⬜ | | Token 更新 SOP 文檔 | ✅ | | Memory 文檔更新 | ✅ | --- ## 附錄: 2026-03-23 事故完整時間線 ``` 11:09:47 - AWOOOI API 啟動,呼叫 logOut (第一個 Token 失效) 11:09:52 - OpenClaw 嘗試啟動失敗 "Logged out" ... 19:31:16 - 給第二個 Token 19:31:27 - 舊 AWOOOI Pod 呼叫 logOut (第二個 Token 失效) 19:31:31 - OpenClaw 啟動失敗 "Logged out" 19:33:00 - 新 AWOOOI Pod 部署完成 (但 Token 已死) ... 19:35:00 - 清空 AWOOOI API Telegram Token 19:35:30 - 重啟 AWOOOI API (不再碰 Telegram) 19:39:01 - 給第三個 Token 19:39:17 - OpenClaw 啟動成功 19:39:36 - 所有系統健康 ```