當團隊允許外部貢獻者提交 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 編譯成功,只能證明該提交在驗證條件下通過,不能證明產生的封存檔值得發佈。驗證工作可能替換建置階段、修改輸出內容,或將額外檔案塞進封存檔。因此,無密鑰路徑產生的應用程式套件、封存檔與匯出目錄,都不應進入可信發佈工作。
可信路徑應執行以下步驟:
- 接收審查系統確認的提交雜湊。
- 在發佈使用者的全新工作區重新取出該提交。
- 驗證目前提交與核准值完全一致。
- 從可信快取或空白快取重新解析相依套件。
- 執行測試,並從原始碼重新封存。
- 只在需要簽署的短暫期間提供憑證。
- 記錄提交雜湊、相依套件鎖定檔摘要與最終產物摘要。
如果必須從驗證路徑傳遞測試報告,只能傳遞不可執行的資料,並在可信端依嚴格格式解析。不要傳遞 Shell 腳本、外掛程式、編譯快取,或可直接放入應用程式套件的檔案。
將失敗清理與稽核納入驗收
隔離方案最容易在失敗分支中失效。建置遭到取消、程序逾時或機器重新啟動後,暫存目錄、背景程序與日誌仍可能殘留。runner 應在每項工作開始前檢查上次留下的內容,並在結束階段終止屬於該工作的子程序。
上線前可依照以下清單驗收:
- 驗證使用者無法列出發佈使用者個人目錄的內容。
- 外部 PR 日誌中不存在權杖、私鑰內容或憑證路徑。
- 驗證路徑已啟用
CODE_SIGNING_ALLOWED=NO。 - 兩條路徑不共用可寫入的 DerivedData 與工具快取。
- 工作取消後,暫存工作區仍會遭到刪除。
- 可信發佈會依固定提交重新取出程式碼,而不是重複使用驗證目錄。
- 發佈產物可以追溯至提交雜湊、鎖定檔摘要與建置記錄。
- runner 帳戶沒有日常工作不需要的管理員權限。
最後進行一次主動演練:提交一個測試 PR,讓建置腳本嘗試讀取常見敏感變數、存取發佈目錄,並向共用快取寫入檔案。正確結果不是腳本「沒有找到任何東西」,而是這些路徑從權限與流程設計上就無法到達。完成這一步後,外部貢獻驗證才真正與正式發佈解耦。
常見問題
外部 PR 關閉程式碼簽名後就足夠安全嗎?
不夠。仍需移除密鑰環境變數、隔離系統使用者與快取、限制工作區權限,並確保任務無法存取可信發佈帳戶。
外部 PR 驗證產生的封存檔可以直接發佈嗎?
不建議。程式碼核准後應由可信流程重新取得固定提交並從原始碼建置,避免不可信二進位產物跨越隔離邊界。
為下一次 Xcode 建置選擇持續在線的雲端 Mac
確認晶片、記憶體、儲存空間、計費週期及節點區域後,即可進入設定流程。實際可用狀態以控制台即時回傳為準。