12 KiB
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_resource 用 instance (IP:port) 代替 component label → Jaccard 服務相似度為 0 |
webhooks.py 優先取 component label |
1fb0c0c |
| #6 | python:3.11-slim 無 openssh-client,ssh 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-09,20 個 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_category 是 backup_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 tool;DecisionManager 不可呼叫 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.110wooo@192.168.0.120wooo@192.168.0.121ollama@192.168.0.188
- Runtime config:
SSH_MCP_HOST_USERS=192.168.0.188=ollama
- Runtime known_hosts:
ssh-mcp-key.data.known_hostsmust 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:22192.168.0.120:22192.168.0.121:22192.168.0.188:22
awoooi-executorRBAC:- read
jobs.batch,cronjobs.batch - read
persistentvolumeclaims - read Velero
backups,backupstoragelocations,backuprepositories,podvolumebackups,podvolumerestores,restores,schedules - patch
statefulsets.apps/daemonsets.appsonly for safe rollout restart
- read
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 後成功/失敗必須寫 KM;executor 前 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_executekey_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
安全注意事項
-
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
- 110:
-
修復命令只能
docker compose up -d或systemctl restart,無法執行任意命令 -
日誌位置:
~/.repair-bot.log(各主機各自記錄)
相關文件
scripts/repair-bot/repair-bot-110.shscripts/repair-bot/repair-bot-188.shapps/api/src/services/host_repair_agent.pyk8s/awoooi-prod/04-repair-ssh-key-template.yamlops/monitoring/alerts-unified.yml