自建 Runner 不隔離 workspace 形同裸奔!CI/CD 安全大坑千萬別踩

2026 全員推行一 Job 一 Workspace,現在已是行業標配

Cloud Mac AI Stack · L1  ·  2026.06.09  ·  約 11 分鐘閱讀  ·  runbook · 含可抄配置

自建 self-hosted runner CI/CD 安全隔離與一 Job 一 Workspace 行業標配示意

許多團隊在 Cloud Mac 或 Mac mini 上掛了 self-hosted GitHub Runner,以為「不排隊、能跑 xcodebuild」就算過關——排隊與 TCO 篇解決的是這一層。

真正的大坑在下一層:共享 workspace。共用 DerivedData、全域依賴快取、一把 PAT 同時給 CI 與 Agent 用——在只有維護者改 workflow 時,這只是少等幾分鐘建置;接上 fork PR、惡意 post step,以及 Claude Code / OpenHands 自動改 .github/workflows 之後,不隔離的自建 runner 形同裸奔:上一個 job 落的密鑰,下一個 job 仍讀得到。

2026 年行業正在對齊的底線也在這裡:一 Job 一 Workspace——每個 job 獨占目錄、結束後清理,並配合 token 輪換。本篇是 Cloud Mac AI Stack · L1 專題第三篇(前置:① 執行引擎 · ② 排隊與取捨):先講 CI/CD 安全大坑從哪來,再給你可抄 runbook。專题目錄見 § L1 連載

讀本篇前 · L1 專題與 Stack 入口

L0 底座買還是租 Cloud Mac · 遷雲端 AI 工作站

L1 專題(建議順序讀)① 執行引擎② 排隊與 TCO③ 本篇 · CI/CD 安全與一 Job 一 Workspace

同棧常一起出現(L3–L5):Claude Code 工作流 · MCP 安裝 · Ollama 並行排班 · OpenClaw 流水線

對比圖:以前怎麼配 vs 2026 行業標配 · 坑踩在哪

很多團隊從託管 macos-latest 遷到 self-hosted 時,只繼承了「不排隊」(見 L1 ②),卻沒繼承託管機的「每 job 一塊新磁碟」。下面這張總覽表 + 示意圖,標出以前覺得「省事」、現在看是「裸奔」的位置。

必看 · 核心對比

以前「省事」= 現在「裸奔」——七條對照一眼看清

以前常見做法 2026 行業標配 踩坑後果

維度 以前常見做法(2024 前後) 2026 行業標配 踩坑後果
Job 工作目錄 多個 job 共用 _work,不刪 run 目錄 一 Job 一 Workspace,job 結束即清 Job B 讀到 Job A 的 .env、腳本植入物
建置快取 共享 DerivedData / 全域 actions/cache key 倉庫級 cache key + 定期 prune 快取投毒;fork PR 掃描全域快取
憑證 一把 PAT 通吃 CI + Agent;secrets 輪換但不刪盤 CI / MCP 分 token;30–90 天輪換 + 清 workspace 磁碟上 PAT 副本長期有效
簽名材料 臨時 keychain 落在 $HOME,無 post 刪除 job 級 keychain,if: always() 銷毀 下一 job 或惡意 step 匯出憑證
誰改 workflow 預設只有 2–3 個維護者 維護者 + fork PR + Agent 改 CI 攻擊面從「人」擴到「半自主進程」
Runner 分工 一台 Mac 接所有倉庫、所有 PR 生產 / 預發 / fork 用不同 label 低信任 workflow 摸到生產簽名環境
託管 vs 自建錯覺 「自建 = 自己的機器,怎麼快怎麼來」 自建 = 自己畫安全邊界(託管 VM 自動隔離) 以為上了 self-hosted 更安全,實則更裸奔

以前為什麼能忍? 私有倉庫、無 fork PR、日更 CI 只有內部 job——共享盤只是少等 3–8 分鐘。 2026 為什麼必須改? 同一台 Cloud Mac 上往往還有 Agent、MCP 與多倉庫 job,任何一條舊做法都會把「省事」變成安全事件。

示意圖 · 以前:共享 workspace 時坑在哪(跨 job 洩漏)
  同一台 self-hosted runner(同一 macOS 使用者)

  Job A · nightly 簽名
       │
       ├─► 解壓 *.mobileprovision、臨時 keychain
       ├─► 寫入 ~/Library/Developer/Xcode/DerivedData   ← 以前覺得「提速」
       └─► PAT 寫入 ~/.netrc(腳本偷懶)                ← 以前覺得「省事」
                │
                │  未清理 · _work 與快取殘留  Job B · 白天 fork PR「修文件」
       │
       └─► post 步驟掃描 _work / DerivedData / .netrc  ← 坑:讀到 Job A 殘留

       結果:自建 runner 形同裸奔(託管 macos-latest 每 job 會丟整塊 VM 磁碟)
示意圖 · 現在:一 Job 一 Workspace(2026 標配)
  Job A  ──►  workspace A(_work/.../run-id)  ──►  if: always() 清理  ──►  ✓
  Job B  ──►  workspace B(全新目錄)          ──►  if: always() 清理  ──►  ✓
                │
                ├─ 敏感快取:帶 repo 前綴的 cache key,或納入 cron prune
                ├─ PAT:CI 與 MCP 分 token,定期輪換
                └─ 高信任 / 低信任 job:不同 runner label(必要時第二台 Cloud Mac)

       結果:Fact 層可審計;與「Claude Code 生產 Diff,Runner 生產 Fact」一致

一眼對照 · 以前 vs 現在

  • 以前踩坑:把 self-hosted 當「更快的伺服器」,共享一切能共享的
  • 現在標配:把 self-hosted 當「你要負責消毒的執行環境
  • 最大誤區:從 macos-latest 遷過來時,只遷了 CPU,沒遷隔離

裸奔從哪來:自建 Runner 最常見三類 CI/CD 安全大坑

託管 macos-latest 每次 job 近似一塊新磁碟,所以很多團隊第一次自建 runner 時誤以為「機器是自己的,怎麼省事怎麼來」。self-hosted 的真實代價是:安全邊界要你自己畫——這也是 L1 專題第二篇 裡「不排隊」之外的隱性帳單。

下面三類誤配,我們在 Cloud Mac 客戶現場見過不止一次;任意踩中兩條,就值得立刻上「一 Job 一 Workspace」。

2026 為什麼全員推行一 Job 一 Workspace

不是 GitHub 突然發了新規定,而是攻擊面變大了:執行 workflow 的不只有維護者,還有 fork PR、供應鏈腳本,以及同機上的 AI Agent。下面三條,解釋了為什麼「共享盤提速」在 2026 已經站不住腳。

1. 跨 job 污染:上一個 job 的密鑰,下一個 job 還能讀到

self-hosted runner 的 _work 目錄在 job 之間預設不會自動消毒。job A 解壓的簽名憑證、腳本寫的 .envpost 步驟落下的 .netrc,job B 完全可能透過相對路徑或符號連結讀到。2026 年攻擊面更大:Claude CodeOpenHands 還能在同機改 .github/workflows——你要審計的不只是 diff,還有改 CI 時磁碟上還有什麼殘留

2. 全域快取投毒:DerivedData / npm 快取變成「公共後門」

共享 ~/Library/Developer/Xcode/DerivedData 或寬口徑 actions/cache key,在內部倉庫裡也許還能忍;一旦接 fork PR,惡意 step 可在 post 掃描全域快取——託管 VM 會銷毀,self-hosted 不清理就是持久攻擊面。iOS 團隊典型翻車:nightly 簽名 job 與白天「文件修復」PR 共用同一 runner。

3. 長期 PAT 落盤:只輪換 secrets 不夠,磁碟上的副本還在

許多 Cloud Mac 棧裡,同一把 GitHub PAT 既給 MCP 拉倉庫,又給 Runner 推製品。Agent 側一旦誤暴露,CI 側也跟著淪陷。只輪換 GitHub UI 裡的 secrets 而不清 workspace,等於只換了鎖芯、沒收回已經寫在磁碟上的副本

Stack 分工 · Fact 層不能裸奔

系列 Slogan(出自L1 專題開篇):Claude Code 生產 Diff,GitHub Runner 生產 Fact。 Diff 再漂亮,Fact 若在「髒 workspace」裡跑綠,merge 之後照樣爆雷。L4 MCP 最小權限 管 Agent token;L1 本篇管磁碟與 job 邊界——兩層都要做,不能只選其一。

3
常見 CI/CD 安全大坑
1
Job 級 workspace 邊界
2026
行業標配元年

先對齊模型:GitHub Actions 在 runner 上留下什麼

很多人把「workspace」理解成 checkout 出來的 git 目錄——其實只是一部分。一次 macOS job 結束後,磁碟上可能還存在:

路徑 / 物件 典型內容 不清理的風險
_work/<repo>/<run-id>/ checkout、建置產物、測試輸出 下一 job 讀取原始碼外的生成物、惡意植入腳本
~/.npm~/Library/Caches 依賴與工具快取 快取投毒、跨倉庫依賴混淆
DerivedData.swiftpm Xcode / Swift 建置快取 洩露符號、嵌入舊版簽名配置
臨時 keychain、*.mobileprovision 簽名材料 高危:下一 job 或惡意 step 匯出憑證
環境變數注入檔、.netrc CI 腳本寫的憑證 明文 PAT 長期留存

託管 runner 在 job 結束時會丟棄整塊磁碟;self-hosted 不會。Agent 棧裡更糟:Claude Code 的會話檔案、Ollama 權重、Runner 的 _work 可能共用同一使用者家目錄(同機排班見L2 並行調度)——「一 job 一 workspace」的意思,是 job 開始時假設環境已被 Agent 或其它 job 弄髒,結束時只保留應跨 job 存在的層(通常是沒有)。

行業標配落地:「一 Job 一 Workspace」指什麼

可以拆成四層,從易到難:

  1. 目錄隔離:每個 job 使用獨立的 RUNNER_TEMP / run 目錄;禁止腳本寫死 /tmp/shared 或倉庫外的「團隊公用資料夾」。
  2. 進程邊界:同一 runner 進程可以串行跑多個 job,但不得在 job 之間共享未清理的全域狀態(例如 export 進 ~/.zshrc 的 API key)。
  3. 憑證邊界:簽名用的臨時 keychain 在 job 的 post 裡刪除;secrets 只透過環境變數注入,不落盤;若必須落盤,路徑必須在 run 目錄內且隨 job 刪除。
  4. 維運邊界:為高風險倉庫配置僅跑受信 workflow 的專用 runner(label 分離),與跑 fork PR 的 runner 物理分開——這在 Cloud Mac 上通常意味著第二台節點,而不是同一台 Mac 上賭清理腳本永遠正確。

與「ephemeral runner」的關係

GitHub 企業版提供的 ephemeral self-hosted runner 會在每個 job 後退出進程並換新實例,本質上把「一 job 一 workspace」自動化。若你用的是常駐 runner(Cloud Mac 上最常見),就要用腳本 + workflow 約定手動達到相近效果。

token 輪換:為什麼不能只清目錄

workspace 清理解決「檔案殘留」;token 輪換解決「即使檔案被拷走,拷貝也會過期」。建議至少輪換三類憑證:

  • Runner 註冊 token:移除並重新註冊 runner(或按組織策略輪換),避免舊機上的註冊狀態被濫用。
  • 用於 CI 的 GitHub PAT / App:scope 最小化(只讀倉庫 vs 寫 packages),與MCP 側 PAT 策略分開管理,避免「一個 token 通吃 Agent + CI」。
  • Apple 簽名與第三方 API key:使用短期憑證或每次 job 從 secrets 注入,禁止寫進 runner 使用者主目錄的 plist。

輪換頻率沒有銀彈:私有倉庫、無 fork PR、workflow 僅維護者可改,90 天一輪通常夠用;若接開源 contributor 或 Agent 自動提交 workflow,建議縮短到 30 天,並在每次安全事件後立即移除 runner 重新註冊。

Runbook:把「一 Job 一 Workspace」寫進 workflow

下列片段可直接貼進現有 pipeline,把行業標配落成可審計步驟。假設 macOS self-hosted runner,路徑按 GitHub 預設 _work 佈局。若同一 Cloud Mac 上還跑 Claude Code,優先讓 Runner 使用獨立 macOS 使用者。MCP 連通見 安裝教程

workflow · job 結束清理(示意)
# .github/workflows/ios-ci.yml 片段
jobs:
  build:
    runs-on: [self-hosted, macos, cloud-mac]
    steps:
      - uses: actions/checkout@v4
      - name: Build iOS
        run: xcodebuild -scheme App -destination 'platform=iOS Simulator,name=iPhone 16' build
      - name: Scrub workspace (always)
        if: always()
        run: |
          rm -rf "$RUNNER_TEMP"/*
          rm -rf "$GITHUB_WORKSPACE"/build
          security delete-keychain ci_temp.keychain-db 2>/dev/null || true
#!/usr/bin/env bash
# /usr/local/bin/runner-prune-work.sh · 每日 03:00 cron
set -euo pipefail
WORK_ROOT="${HOME}/actions-runner/_work"
# 刪除超過 48h 的 run 目錄(按 mtime)
find "$WORK_ROOT" -mindepth 3 -maxdepth 3 -type d -mtime +2 -exec rm -rf {} +
# 可選:修剪 DerivedData 中超過 7 天的條目
find ~/Library/Developer/Xcode/DerivedData -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} + 2>/dev/null || true

驗證方式:故意在兩個連續 job 裡列印 ls -la "$GITHUB_WORKSPACE/.." 與關鍵快取路徑,確認第二個 job 看不到第一個 job 寫入的標記檔案(例如在 post 裡 touch /tmp/job-marker-$GITHUB_RUN_ID 並檢查是否殘留)。

常見誤配

在 workflow 裡用 actions/cache 快取未簽名的第三方二進位到全域 cache key,卻未限制倉庫範圍——這相當於在 runner 上建「跨 job、跨倉庫」的共享層。要麼收緊 cache key 與分支,要麼把快取目錄納入 prune 腳本。

Cloud Mac 同機部署:Agent + Runner 的安全分界線

典型 Cloud Mac AI Stack 是:Claude Code(Diff)+ Runner(Fact)+ 可選 Ollama 同機。省排隊、省 git pull,但若共用不可清理的全域目錄,CI 裸奔會牽連 Agent 會話

  • 使用者分離:Runner 用 runner 使用者,Agent 用開發者使用者;禁止在同一 ~/.zshrc 混放 ANTHROPIC_API_KEY 與簽名密鑰。
  • Agent 工作區 ≠ CI workspace:Claude Code 的 project 目錄不要指向 Runner 的 _work;Agent 生成的 patch 走 git,不直接寫 CI 快取樹。
  • 記憶體爭用 ≠ 可共享磁碟:Ollama 與 Runner 搶記憶體見 並行排班;Swap 高不能成為「繼續共享 DerivedData」的藉口。
  • 出口 IP 與 label:能存取內網預發的 runner 不要同時接 fork PR;Agent 自動提交的 workflow 先走低權限 label,人工晉升後再碰生產 runner。

什麼時候可以暫時不做(以及何時必須立刻做)

場景 可否暫緩 說明
私有 monorepo、僅 2–3 維護者、無 fork PR 短期可 仍建議每月 manual prune + 季度 token 輪換
開源倉庫接受外部 PR 跑 Actions 不可 應專用 runner 或改託管 macOS
已接 Claude Code / OpenHands / MCP 寫倉庫 不可 預設一 Job 一 Workspace,禁止共享敏感快取
簽名憑證在 CI 解密 不可 必須 job 級 keychain + post 刪除

上線前檢查清單(可列印)

  • 每個 job 有 if: always() 的清理 step 或等價宿主機 prune
  • 臨時 keychain / 簽名檔案不落在 $HOME 永久路徑
  • 高風險倉庫與低信任 workflow 使用不同 runner label
  • Runner 註冊 token 與 CI PAT 有輪換日曆(建議 30–90 天)
  • 新 contributor 的首個 workflow PR 走人工 review,而非直接命中生產 runner
  • Agent 與 CI 使用不同 PAT / App,禁止一把 token 通吃 MCP 與 Runner
  • L1 專題第二篇對照:② 解決「慢」,③ 本篇解決「自建 runner 是否裸奔」

L1 專題連載 · 與 Stack 各層怎麼接

本篇收束 L1(Fact 層) 安全線:從「為什麼要有 Runner」→「值不值得自建」→「2026 一 Job 一 Workspace 行業標配怎麼落地」。建議按表順序讀;縱向回 L0,橫向接 L3–L5。

篇目 主題 狀態
· 執行引擎為什麼 Runner 是 Cloud Mac AI Stack 的 L1(Diff → Fact)已發布
· 排隊與 TCOmacOS CI 排隊時間 · self-hosted vs macos-latest已發布
· 本篇自建 Runner 安全 · 一 Job 一 Workspace 行業標配已發布
· OpenClaw 流水線Runner 執行 step · OpenClaw 編排觸發與回執(L1 延伸)已發布

Stack 縱向銜接(每層一篇入口即可):

讀完 L1 三連後,若你已在 Cloud Mac 上同時跑 Agent 與 CI,下一層通常是補全 L3 Diff 層決策(Claude Code 為何替代傳統 IDE 的獨立首講,與vs Cursor工作站文分工不同)——再往後是 L6 端到端閉環地圖(排期中)。

常見問題

一 Job 一 Workspace 會不會讓 CI 更慢?
會冷啟動變慢——這正是當年共享盤的原因。2026 的合理做法是:只刪敏感物與本 job 目錄,可再生快取用帶倉庫前綴的 cache key,而不是永不清理的全域 DerivedData。

自建 runner 算裸奔嗎?什麼情況下算?
若多個 job / 多個倉庫共用同一使用者家目錄裡的簽名材料、.netrc 或寬口徑全域快取,且沒有 if: always() 清理——在接 fork PR 或 Agent 改 workflow 時,就算裸奔

能不能只收緊 MCP 權限,繼續共享 runner 磁碟?
不夠。MCP 最小權限 管 Agent 調工具;Runner 側仍要管磁碟上已存在的檔案。惡意 fork workflow 不走 MCP,照樣能掃 _work 殘留。

GitHub 官方有沒有一鍵開關?
託管 runner 近似「一鍵」;self-hosted 需組合 workflow post、宿主機 cron、以及可選的 ephemeral 模式。沒有單條 actions/checkout 參數能替代全套邊界。

和 OpenClaw 編排層怎麼分工?
OpenClaw 管觸發順序與回執;Runner 管逐步執行。隔離策略寫在 workflow 與 runner 宿主機,不應假設 OpenClaw 會替你做磁碟清理(見 L1 專題④ · OpenClaw 流水線)。

L1 專題要從哪篇開始讀?
建議 ① 執行引擎② 排隊③ 本篇。完整表見 § L1 連載

L1 專題已三連 · 下一層 Diff

Fact 層做完,該補 Claude Code 了

L1 解決「CI 在什麼環境裡跑 Fact」。下一步通常是 L3:Cloud Mac 上的 Claude Code 工作流與 IDE 替代邏輯(與 vs Cursor 專題文分工不同)。

閱讀 Claude Code 工作流
Cloud Mac 查看 M4 套餐