Files
awoooi/docs/adr/ADR-058-host-auto-repair-ssh-whitelist.md

12 KiB
Raw Permalink Blame History

ADR-058: Host Auto-Repair SSH 白名單架構

狀態: 已接受
日期: 2026-04-05 (台北時區)
起草: Claude Code (Sprint 3 Host Auto-Repair)
審查: 首席架構師 (同日)


背景

Phase O 可觀測性補完後系統能偵測主機層服務異常SentryDown、HarborDown、GiteaDown 等),但缺少從 K8s API Pod 向主機層發送修復命令的能力。需要一個安全、可審計的機制讓 AWOOOI API 透過 SSH 執行受限的修復操作。


決策

D1 — 使用 SSH command= forced command 白名單

在目標主機的 authorized_keys 中使用 command= 選項,強制所有使用特定 key 的 SSH 連線只能執行指定的白名單腳本,無論呼叫方送出什麼命令。

command="/home/wooo/bin/repair-bot-110.sh",no-pty,no-agent-forwarding ssh-ed25519 ...

優點

  • SSH key 洩漏也只能執行白名單操作(最小權限)
  • 不需要在 API Pod 中安裝任何 agent
  • 完整日誌記錄repair-bot 寫 ~/.repair-bot.log

D2 — 命令格式:repair:<component>

白名單腳本接受 repair:<component> 格式component 對應預設的修復動作docker compose up -d 或 systemctl restart

Regex 兩端統一:^repair:([a-z0-9][a-z0-9-]{0,30})$

D3 — SSH Key 儲存在 K8s Secret掛載到 API Pod

volumes:
  - name: repair-ssh-key
    secret:
      secretName: awoooi-repair-ssh-key
      defaultMode: 0400  # 八進位 0400 = 十進位 256 = r--------

掛載路徑:/etc/repair-ssh/id_ed25519

D4 — HostRepairAgent 透過 ActionType.SSH_COMMAND 整合到 Playbook 系統

Prometheus/SigNoz 告警 → webhook → auto_repair_service
  → 比對 Playbook (symptom_pattern)
  → _execute_step(ActionType.SSH_COMMAND)
  → HostRepairAgent.repair(layer, component)
  → SSH forced command
  → repair-bot.sh → docker compose up -d

D5 — layer 路由隔離

Layer 主機 使用者 修復類型
docker-110 192.168.0.110 wooo Docker Compose
docker-188 192.168.0.188 ollama Docker Compose
systemd-188 192.168.0.188 ollama systemd
k8s kubectl不走 SSH拒絕

已批准的修復白名單

110 主機 (repair-bot-110.sh)

Component 修復方式 目錄
sentry docker compose up -d /opt/sentry
harbor docker compose up -d /home/wooo/harbor/harbor
gitea docker compose up -d /home/wooo/gitea
gitea-runner docker compose up -d /home/wooo/act-runner
langfuse docker compose up -d /home/wooo/langfuse
alertmanager docker compose up -d /home/wooo/monitoring
signoz docker compose up -d /home/wooo/signoz/deploy/docker
stock-platform docker compose up -d /home/wooo/stockPlatform (2026-04-09 新增)

188 主機 (repair-bot-188.sh)

Component 修復方式 目錄
openclaw docker compose up -d /home/ollama/clawbot-v5
minio docker compose up -d /home/ollama/minio
signoz docker compose up -d /home/ollama/signoz/deploy/docker
momo-app docker compose up -d /home/ollama/momo-pro (2026-04-09 新增)
tsenyang-website docker compose up -d /home/ollama/services/tsenyang (2026-04-09 新增)
bitan-app docker compose up -d /home/ollama/services/bitan (2026-04-09 新增)

Appendix A — Bug 修復記錄 (2026-04-09)

執行者: Claude Sonnet 4.6 (Asia/Taipei) 背景: 自動修復機制從未成功執行success_count 全部為 0完整審計後發現下列阻斷性問題

Bug 問題描述 修復 Commit
#5 target_resourceinstance (IP:port) 代替 component label → Jaccard 服務相似度為 0 webhooks.py 優先取 component label 1fb0c0c
#6 python:3.11-slimopenssh-clientssh binary 缺失 Dockerfile 生產 stage 加 openssh-client 1fb0c0c
#11 NetworkPolicy 未開放 22 (SSH),預設拒絕所有未列 port 02-network-policy.yaml 新增 110:22 + 188:22 egress 07a097c
#12 Secret defaultMode=0400 (root-only)appuser (UID 1000) 無法讀 SSH key Pod securityContext.fsGroup: 1000 77f2da9

E2E 驗證結果

# docker-110
SentryDown → Jaccard 匹配 sentry-down-repair → SSH wooo@192.168.0.110 repair:sentry
→ REPAIR_OK:sentry (6208ms) ✅

# docker-188
MoWoooWorkDown → Jaccard 匹配 momo-app-down-repair → SSH ollama@192.168.0.188 repair:momo-app
→ REPAIR_OK:momo-app (3791ms) ✅

Playbook 覆蓋矩陣 (2026-04-0920 個 Playbook)

告警 Playbook Layer success_count
SentryDown sentry-down-repair docker-110 1
HarborDown harbor-down-repair docker-110 0
GiteaDown gitea-down-repair docker-110 0
AlertmanagerDown alertmanager-down-repair docker-110 0
OpenClawDown openclaw-down-repair docker-188 0
MoWoooWorkDown momo-app-down-repair docker-188 2
TsenyangWebsiteDown tsenyang-website-down-repair docker-188 0
BitanWoooWorkDown bitan-app-down-repair docker-188 0
StockWoooWorkDown stock-platform-down-repair docker-110 0
SignOzDown signoz-down-repair docker-188 0
DockerContainerExited docker-container-exited-repair 動態 0
DockerContainerUnhealthy docker-container-unhealthy-repair 動態 0
KubePodNotReady k8s-pod-not-ready-restart k8s 0

注意: openclaw-down-repair 原錯誤指向 docker-110,已於 2026-04-09 修正為 docker-188


Appendix B — Backup Failure Route Parity (2026-05-01)

HostBackupFailed / backup 類告警的 alert_categorybackup_failure。它必須在所有 host-layer 自動化路徑與 host_resource 同級處理:

Layer 必須行為
Alertmanager rule-first YAML SSH_DIAGNOSE / NO_ACTION 不進 LLM 覆寫
AutoRepairService backup_failure 視為 host-layer拒絕 K8s Playbook fallback
DecisionManager kubectl 動作在 kubectl parser 前路由 SSH MCP
DecisionManager K8s guard backup_failure 產生 kubectl 時降級 emergency escalation
Telegram buttons backup_failure 顯示只讀診斷按鈕:主機磁碟、備份 Job、Velero 狀態

2026-05-01 根因DecisionManager SSH route 只含 infrastructure / host_resource,漏掉 backup_failure,導致 ssh 192.168.0.110 '...;...' 類只讀診斷動作掉進 parse_kubectl_action(),被 forbidden_shell_metachar 擋下。

同日按鈕審計也發現 category button 的 friendly provider name 會漂移:callback_action_spec.yaml 使用 k8s / ssh,但 MCP registry 實際名稱是 kubernetes / ssh_host。Dispatcher 必須正規化 provider alias否則卡片按鈕會顯示但執行時變成 provider_not_found

2026-05-01 第二輪 live 驗證補充:ssh_diagnose 必須是 SSHProvider 的正式 read-only toolDecisionManager 不可呼叫 provider 未註冊工具也不可把診斷成功當自動修復完成。SSH 執行失敗必須保留 READY、標記 mcp_all_failed=True,並送 emergency intervention。

Runtime 權限基準

  • K8s Secret:
    • awoooi-repair-ssh-key 掛載 /etc/repair-ssh/
    • awoooi-repair-known-hosts 掛載 /etc/repair-known-hosts/
    • ssh-mcp-key 掛載 /run/secrets/ssh_mcp_key/etc/ssh-mcp/known_hosts
  • Remote authorized_keys:
    • wooo@192.168.0.110
    • wooo@192.168.0.120
    • wooo@192.168.0.121
    • ollama@192.168.0.188
  • Runtime config:
    • SSH_MCP_HOST_USERS=192.168.0.188=ollama
  • Runtime known_hosts:
    • ssh-mcp-key.data.known_hosts must be non-empty and mounted at /etc/ssh-mcp/known_hosts
    • Because the file is mounted with subPath, updating the Secret requires rolling API/worker pods before asyncssh sees the new trust store
  • NetworkPolicy egress:
    • 192.168.0.110:22
    • 192.168.0.120:22
    • 192.168.0.121:22
    • 192.168.0.188:22
  • awoooi-executor RBAC:
    • read jobs.batch, cronjobs.batch
    • read persistentvolumeclaims
    • read Velero backups, backupstoragelocations, backuprepositories, podvolumebackups, podvolumerestores, restores, schedules
    • patch statefulsets.apps / daemonsets.apps only for safe rollout restart

If SSH MCP fails, the incident must not silently become a manual approval card; it must raise the emergency intervention path with the exact SSH failure reason when available.


Appendix C — LLM Ghost Loop Controls (2026-05-01)

Alertmanager 重複 delivery、修復失敗後告警仍 firing、以及 provider timeout/500 會形成「告警 → LLM → 修復失敗 → 20 秒後再 LLM」的鬼循環。這不是 AI 訂閱費用問題,而是飛輪收斂問題。

必須維持的控制

控制 行為
Alertmanager in-flight lock 同一 fingerprint 排入背景 LLM 前必須取得 Redis alertmanager:llm_inflight:{fingerprint}TTL 10 分鐘
Stable LLM cache key 有 alert context 時OpenClaw cache key 不得使用完整動態 prompt必須使用 prompt family + stable alert dimensions
No retry storm Alertmanager webhook 已完成來源驗證後,內部處理錯誤應回 2xx degraded accepted不回 500 觸發 Alertmanager 重試
Learning after blocked automation executor 後成功/失敗必須寫 KMexecutor 前 guard block 也應優先沉澱成 KM/AOL/timeline避免下一次同告警重跑完整 LLM
Paid provider policy Gemini/Groq/Claude/OpenRouter 只能作為 rate-limited fallback 或高複雜度專家路徑;不得用付費 provider 掩蓋 dedupe/cache/retry 問題

健康狀態判準

  • 同一 fingerprint 10 分鐘內最多一個背景 LLM analysis task。
  • 重複告警應命中 DB convergence、Redis in-flight lock、rule-first、Playbook/KM/RAG、或 LLM cache 其中一層。
  • 外部 provider 用量應接近「新問題數」而不是「告警 delivery 數」。
  • HTTP 500 比例不得由已接收告警的後處理錯誤造成;若有 500需先確認是否在來源驗證/反序列化前失敗。

首席架構師 Review 記錄 (2026-04-05)

評分:72/100 → 修正後 88/100

已修正問題:

  • C1: _ssh_execute key_path 改為直接傳入,不反查 LAYER_SSH_CONFIG
  • C2: PlaybookService.create() proxy 方法Router 不直接呼叫 _repository
  • C3: CD Step 1b sed 替換 IMAGE_TAG_PLACEHOLDER消除中斷風險
  • M3: repair-bot regex 統一為 [a-z0-9][a-z0-9-]{0,30},禁止底線
  • m1: defaultMode 加八進位說明注釋
  • m2: _ssh_execute 用 deadline 計算剩餘 timeout

待觀察問題(已知、可接受):

  • M1: 測試使用 AsyncMock豁免原因_ssh_execute 是真實 subprocess無法在 CI 無 SSH 環境直接測試;已透過 E2E 驗收補充覆蓋)
  • M2: 短期使用 accept-new,後續改為 pinned known_hosts ConfigMap

安全注意事項

  1. SSH key 輪換:若 awoooi-repair-ssh-key 洩漏,立即執行:

    • 110: sed -i '/repair-bot/d' ~/.ssh/authorized_keys
    • 188: sed -i '/repair-bot/d' ~/.ssh/authorized_keys
    • 重建 keypair 並更新 K8s Secret
  2. 修復命令只能 docker compose up -dsystemctl restart,無法執行任意命令

  3. 日誌位置~/.repair-bot.log(各主機各自記錄)


相關文件

  • scripts/repair-bot/repair-bot-110.sh
  • scripts/repair-bot/repair-bot-188.sh
  • apps/api/src/services/host_repair_agent.py
  • k8s/awoooi-prod/04-repair-ssh-key-template.yaml
  • ops/monitoring/alerts-unified.yml