2026-03-23 重大事故修復與治理: 1. ADR-010: Secrets 集中管理 (Bitwarden + Sealed Secrets) 2. ADR-011: NetworkPolicy 變更治理 (偵測 + 告警 + 人工決策) 3. ADR-012: 危險操作治理 (Tier 分級 + CI/CD 攔截 + 審計) 4. UX-001: 告警疲勞解決方案 (時間衰減 + 智慧分組) CLAUDE.md 更新: - 新增最高優先級鐵律 (禁止 ClawBot、OpenClaw 核心、禁止危險 API) - 新增任務開始前必讀 Memory 對照表 事故教訓: - Telegram Token 連續三次被 logOut 失效 - AWOOOI API 程式碼呼叫 logOut 導致災難 - 已停用 AWOOOI API Telegram,OpenClaw 為唯一 Gateway Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
371 lines
17 KiB
Markdown
371 lines
17 KiB
Markdown
# 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 <container> │
|
||
│ - 等待所有實例完全終止 │
|
||
│ │
|
||
│ 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 - 所有系統健康
|
||
```
|