VMArm 技術紀錄

雲端 Mac CI 外部 PR 隔離:無密鑰建置與可信發佈

雲端 Mac CI 外部 PR 隔離:無密鑰建置與可信發佈

當團隊允許外部貢獻者提交 PR 後,雲端 Mac CI 面對的問題便不再只是「程式碼能否編譯」。建置腳本本身也是需要審查的輸入:它可以讀取環境變數、搜尋使用者目錄、修改共用快取,甚至替換後續工作會使用的工具。安全目標必須明確:外部 PR 可以驗證編譯與測試,但不能取得發佈憑證,也不能把產物直接送入正式發佈流程。

先劃分兩條信任路徑

將工作分成「無密鑰驗證」與「可信發佈」兩條路徑。前者接收外部 PR,只執行相依套件解析、編譯、靜態檢查,以及不需要真實憑證的測試;後者只接受已經審查並合併的固定提交,負責簽署、封存與交付。

在 VMArm 的專用實體節點上,可以為兩條路徑建立不同的 macOS 系統使用者。驗證使用者不加入管理員群組、不保存發佈憑證,也不能讀取發佈使用者的個人目錄。發佈使用者同樣不得重複使用驗證使用者可寫入的腳本、快取或工具目錄。

系統使用者隔離並不等同於虛擬機邊界。它可以大幅降低一般腳本誤讀憑證或污染檔案的風險,但不能取代系統更新、最小權限原則,以及對不可信程式碼的人工審查。

建議採用以下邊界:

項目 無密鑰驗證 可信發佈
程式碼來源 外部 PR 的固定提交 已審查分支的固定提交
程式碼簽署 停用 依發佈流程啟用
工作區 每次重新建立 獨立目錄
快取 可丟棄、不可跨信任域使用 僅由可信工作共用
建置產物 只用於檢查 重新建置後交付
憑證 不注入 需要時短暫注入

建立最小權限工作區

不要讓 runner 長期在使用者個人目錄的根層級進行建置。請為驗證工作建立專用根目錄,並確保其他本機使用者無法讀取。每項工作開始時使用暫存目錄,結束時無論成功或失敗都要清理。

set -euo pipefail

umask 077
RUN_ROOT="$HOME/ci-untrusted"
mkdir -p "$RUN_ROOT"
chmod 700 "$RUN_ROOT"

WORK_DIR="$(mktemp -d "$RUN_ROOT/job.XXXXXX")"
cleanup() {
  chmod -R u+rwX "$WORK_DIR" 2>/dev/null || true
  rm -rf "$WORK_DIR"
}
trap cleanup EXIT INT TERM

cd "$WORK_DIR"
printf 'user=%s\nworkspace=%s\n' "$(id -un)" "$WORK_DIR"

執行 rm -rf 前,必須確定路徑來自目前工作建立的暫存目錄,不能直接拼接分支名稱。分支名稱可能包含斜線、空格或刻意構造的字元,適合當作顯示文字,卻不適合未經處理便放入檔案路徑。

取出原始碼後,請記錄提交雜湊並輸出至日誌。後續可信路徑必須依照這個不可變識別碼重新取得程式碼,不能只依賴仍可能繼續變動的分支名稱。

在執行建置前證明環境中沒有密鑰

「沒有主動使用密鑰」不代表「工作無法讀取密鑰」。runner 服務可能繼承啟動環境,Shell 初始化檔案也可能附加權杖。呼叫專案腳本前應先檢查變數名稱,並且只將必要變數傳給建置命令。

blocked='TOKEN|SECRET|PASSWORD|PRIVATE|SIGNING|AUTH|CREDENTIAL'
if env | cut -d= -f1 | grep -Eiq "$blocked"; then
  printf '%s\n' "blocked environment variable detected" >&2
  exit 70
fi

env -i \
  HOME="$HOME" \
  PATH="/usr/bin:/bin:/usr/sbin:/sbin" \
  TMPDIR="$WORK_DIR/tmp" \
  xcodebuild \
    -workspace App.xcworkspace \
    -scheme App \
    -configuration Debug \
    -derivedDataPath "$WORK_DIR/DerivedData" \
    CODE_SIGNING_ALLOWED=NO \
    build

如果實際專案依賴套件管理器或自訂工具,應將通過審查的工具目錄明確加入 PATH,而不是繼承完整的登入環境。檢查變數名稱只是一道門檻;敏感值還可能存在於設定檔、鑰匙圈、代理伺服器設定與 Git 憑證輔助工具中。因此,驗證使用者從一開始就應保持空白,而不是每次建置前才暫時刪除幾個變數。

處理相依套件與快取

外部程式碼可以將內容寫入快取;若可信工作隨後讀取同一份快取,就可能遭到污染。最穩妥的方式是依信任域拆分快取;對於外部 PR,還應進一步依儲存庫或工作編號區分。不要讓驗證路徑寫入發佈使用者的 SwiftPM、編譯模組或自訂二進位工具快取。

還原快取後,應檢查擁有者與權限,並拒絕指向工作區外部的符號連結。可執行工具最好來自唯讀的基準目錄;專案在建置期間產生的腳本,只能在目前的暫存工作區內執行。

不讓產物跨越信任邊界

外部 PR 編譯成功,只能證明該提交在驗證條件下通過,不能證明產生的封存檔值得發佈。驗證工作可能替換建置階段、修改輸出內容,或將額外檔案塞進封存檔。因此,無密鑰路徑產生的應用程式套件、封存檔與匯出目錄,都不應進入可信發佈工作。

可信路徑應執行以下步驟:

  1. 接收審查系統確認的提交雜湊。
  2. 在發佈使用者的全新工作區重新取出該提交。
  3. 驗證目前提交與核准值完全一致。
  4. 從可信快取或空白快取重新解析相依套件。
  5. 執行測試,並從原始碼重新封存。
  6. 只在需要簽署的短暫期間提供憑證。
  7. 記錄提交雜湊、相依套件鎖定檔摘要與最終產物摘要。

如果必須從驗證路徑傳遞測試報告,只能傳遞不可執行的資料,並在可信端依嚴格格式解析。不要傳遞 Shell 腳本、外掛程式、編譯快取,或可直接放入應用程式套件的檔案。

將失敗清理與稽核納入驗收

隔離方案最容易在失敗分支中失效。建置遭到取消、程序逾時或機器重新啟動後,暫存目錄、背景程序與日誌仍可能殘留。runner 應在每項工作開始前檢查上次留下的內容,並在結束階段終止屬於該工作的子程序。

上線前可依照以下清單驗收:

最後進行一次主動演練:提交一個測試 PR,讓建置腳本嘗試讀取常見敏感變數、存取發佈目錄,並向共用快取寫入檔案。正確結果不是腳本「沒有找到任何東西」,而是這些路徑從權限與流程設計上就無法到達。完成這一步後,外部貢獻驗證才真正與正式發佈解耦。

常見問題

外部 PR 關閉程式碼簽名後就足夠安全嗎?

不夠。仍需移除密鑰環境變數、隔離系統使用者與快取、限制工作區權限,並確保任務無法存取可信發佈帳戶。

外部 PR 驗證產生的封存檔可以直接發佈嗎?

不建議。程式碼核准後應由可信流程重新取得固定提交並從原始碼建置,避免不可信二進位產物跨越隔離邊界。

獨享實體 Mac 節點

為下一次 Xcode 建置選擇持續在線的雲端 Mac

確認晶片、記憶體、儲存空間、計費週期及節點區域後,即可進入設定流程。實際可用狀態以控制台即時回傳為準。

選擇雲端 Mac 設定