iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0

替高風險 Eval 架一個真正隔離的 Runner

[[我也希望安全第一]]|第 14/30 天

假設下週要替公司架一個 eval runner

需求看起來很單純:研究團隊要測 Agent 能不能完成資安題目,因此需要 shell、套件安裝和一組可重置的靶機。平台團隊通常會先拿現有 staging 改,因為那裡部署快、資料能重建,大家也習慣共用幾項服務。

真正列權限時,事情開始不對勁。套件要從哪裡下載?runner 能不能解析公司 DNS?雲端 metadata endpoint 是否可達?現有 registry token 還能讀哪些專案?log 要往外送,但 Agent 能不能沿同一條路回寫?

安全評估還可能故意關掉模型防護,交付很難的攻擊任務,允許長時間執行。此時 runner 裡的工作負載可能比一般 production 版本更積極。這套環境不能沿用「反正只是測試」的鬆散習慣。

先畫出真正的 trust boundary

一個比較能工作的架構不是完全沒有外部依賴,而是所有必要依賴都經過狹窄、可替換的 broker:

                    ┌── immutable package mirror
                    │      (固定快照、唯讀)
Agent runner ──┬── egress gateway ── synthetic targets
  ephemeral    │      deny others       (私有測試網段)
  no host mount│
               ├── credential broker ── job-scoped token
               │      no static secret
               └── telemetry sink ──── append-only evidence store

production/corp network/cloud metadata:沒有路由、沒有信任關係

這裡有四個容易被忽略的細節。

第一,套件 mirror 不是任意 HTTP proxy。評估需要哪些套件,可以事先固定 digest;真的要動態安裝,也應經過能檢查目的地和內容的 gateway。第二,runner 不掛 host socket、家目錄或共享 credential cache。第三,每個 job 向 broker 換取短效、單用途身分,job 結束就撤銷。第四,telemetry 往外送的路徑只能 append,Agent 不能回頭修改證據。

Kubernetes 的 default deny 只是起點

若 runner 跑在 Kubernetes,可以先對 namespace 建立出站預設拒絕:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: eval-default-deny-egress
  namespace: agent-eval
spec:
  podSelector: {}
  policyTypes:
    - Egress

接著只開到同 namespace、帶有特定 label 的 egress gateway:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: eval-allow-egress-gateway
  namespace: agent-eval
spec:
  podSelector:
    matchLabels:
      role: agent-runner
  policyTypes: [Egress]
  egress:
    - to:
        - podSelector:
            matchLabels:
              role: controlled-egress
      ports:
        - protocol: TCP
          port: 8443

套用 YAML 不代表隔離已成立。Kubernetes 官方安全清單建議 namespace 採 default deny,也提醒實際能力取決於 CNI;cloud metadata、DNS、IPv6、host network 與同節點旁路都要另外測。

驗證要從盒子裡往外撞

部署後從真實 runner image 執行一組負向測試,並把預期結果寫死:

測試 A:連公開 HTTPS 網站          → 連線失敗,產生 deny log
測試 B:連 169.254.169.254 metadata → 連線失敗,發出 S1 警示
測試 C:直接連 production IP       → 連線失敗,不因略過 DNS 而成功
測試 D:經 gateway 取允許套件      → 成功,只能取得固定 digest
測試 E:跟隨 redirect 到外站       → gateway 拒絕
測試 F:讀取其他 job 的 secret      → IAM 拒絕,記錄 actor 與 resource
測試 G:關掉 policy/DNS 服務       → fail closed,不自動開放網路

還要測被允許的支援服務遭攻破。讓紅隊控制一個假 package response,看看能否藉 redirect、DNS、回呼 URL 或錯誤訊息建立第二條通道。Hugging Face 事件教訓最具體的地方,就是「允許的元件」本身也要當作不可信輸入。

Air gap 不是萬靈丹

完全斷網很適合測不需要外部環境的能力,卻會讓某些真實網安任務失去意義。鎖得太緊,也可能測不出模型發布後會出現的行為。

比較實際的作法是分兩階段:先在無網路環境做能力探測,再把確實需要互動的測試移到獨立私有網段,放入合成網域、假憑證與可重置目標。要測公開網路能力時,則需要更高核准層級、即時監看與明確時間窗,而不是把一般 runner 的開關改成 internet=true。

這些控制都花錢。OpenAI 在 Hugging Face 事件後公布的新監控設計,估計監控本身需要相當於被監控工作約 20% 的算力。對趕 benchmark 的團隊,這很容易被看成拖慢速度的負擔。問題是,一次跨到第三方 production 後,省下來的那 20% 很可能要用更昂貴的事故調查補回去。

「支援 Docker」還不能在架構圖上畫勾

OpenHands 很適合拿來檢查這個誤會。官方提供直接在本機啟動 Agent Server 的方式,也明確警告這會讓 Agent 取得本機檔案系統的完整存取;另一種模式會替每個 conversation 建立獨立 Docker container。後者隔離的是 conversation execution,外層 Canvas 與 Automation 不會因此自動進入同一個 sandbox,掛進去的 host workspace 也仍可能被多個 conversation 共用。

Pi 的核心則不替部署者假裝已經安全:預設 bash 能做到什麼,取決於啟動 Pi 的環境;需要 sandbox 要由外部容器或 extension 補上。Goose 雖然有 manual/smart approval、prompt injection detection 和 adversary reviewer,官方安全說明仍建議把它放進權限受限的 VM 或 container。原因很直接:允許執行的命令仍使用啟動 Goose 的帳號權限,而 adversary reviewer 出錯時採取的是 fail-open。應用層多幾次判斷,沒有改變底層 process 看得到哪些檔案和網路。

這不是說哪個做法一定不安全,而是三者把 trust boundary 畫在不同地方。Pi 把責任明白留給 extension 與外部環境;Goose 在本機執行前增加權限與內容檢查,但真正的硬邊界仍要靠主機、VM 或 container;OpenHands 則直接提供 conversation container,同時留下外層服務與掛載範圍要另外檢查。

因此安裝完後,我不會只跑「能不能修好 bug」,還會從 Agent 裡測:能否讀家目錄、Docker socket、其他 workspace、雲端 metadata、憑證檔與外網。產品功能頁回答的是支援哪種 runtime;負向測試才回答那個 runtime 實際隔離了什麼。

第 15 篇接著處理一件隔離無法保證的事:異常已經發生時,誰有權停、要停到哪裡,以及怎麼避免一按 kill switch 就把證據一起清掉。

參考與來源


上一篇
第 13 篇|17,000 個動作,為什麼沒叫醒值班工程師?
下一篇
第 15 篇|看見異常後怎麼停下來——警示、撤銷權限與 containment plan
系列文
我也希望安全第一——然後我們看看這些鎖是怎麼一個個被撬開的 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言