建置任務在互動式終端機中執行正常,換到無人值守的 runner 後卻出現「無法開啟」、「不允許此操作」,或是程序才剛啟動就結束。許多人第一時間會執行 chmod +x,但如果檔案本來就有執行權限,真正的阻擋來源通常是 Gatekeeper、程式碼簽章評估,或下載檔案所帶的隔離屬性。常見的觸發情境包括從瀏覽器下載工具、複製其他工作階段的產物,以及從會保留延伸屬性的封存檔還原快取。
先區分四類執行失敗
看到「無法執行」時,不要立刻刪除屬性。先收集檔案類型、權限、架構與延伸屬性;這四項結果足以排除大多數誤判。
TOOL="/opt/build-tools/example-tool"
ls -leO@ "$TOOL"
file "$TOOL"
uname -m
xattr -l "$TOOL"
ls 用來確認擁有者與執行位元;file 應顯示與節點相符的可執行檔架構;如果 xattr 顯示 com.apple.quarantine,只能代表檔案曾進入隔離評估流程,不能直接證明檔案安全。若錯誤為 Permission denied,請先檢查目錄是否允許逐層存取,以及掛載點是否設有執行限制。若錯誤為 Bad CPU type in executable,應改用架構正確的產物,而不是處理隔離屬性。
| 現象 | 優先檢查 | 不應立即執行的操作 |
|---|---|---|
| Permission denied | 檔案與上層目錄權限 | 遞迴清除延伸屬性 |
| Bad CPU type | file 與 uname -m |
反覆修改執行位元 |
| 已損毀或無法驗證 | 簽章與 Gatekeeper 結果 | 關閉系統安全檢查 |
| 僅 runner 失敗 | 實際路徑、使用者與快取來源 | 假設互動式環境完全相同 |
隔離屬性是來源線索,不是故障本身。先證明取得的是預期檔案,再決定是否放行。
讀取 Gatekeeper 的明確結論
評估單一二進位檔或應用程式
對命令列工具,先檢查簽章,再要求系統原則提供評估結果。若要檢查應用程式套件,可將 TOOL 改為對應的 .app 路徑。
codesign --display --verbose=4 "$TOOL"
codesign --verify --deep --strict --verbose=2 "$TOOL"
spctl --assess --type execute --verbose=4 "$TOOL"
未簽章的內部工具不一定有問題,但必須來自受控的建置流程,並透過摘要驗證證明其完整性。如果第三方預先編譯工具宣稱帶有簽章,驗證失敗時就應停止使用並重新取得可信產物,不能靠刪除隔離屬性掩蓋簽章變更。
查詢原則日誌
彈出式視窗與 runner 的標準錯誤通常會省略原因。重現問題後,立即讀取近期的安全原則記錄,即可確認實際接受評估的路徑,避免檢查的是快取 A,真正執行的卻是快取 B。
log show --last 10m \
--predicate 'subsystem == "com.apple.security.syspolicy"' \
--style compact
將日誌中的路徑逐一與 command -v、runner 設定及指令碼變數核對。符號連結尤其容易造成偏差:入口位於工具目錄,但最終檔案可能來自舊快取。
放行前先驗證產物身分
最穩妥的流程,是由工具發布方或內部產物流程提供 SHA-256 摘要,並將摘要檔與工具版本一起納入儲存庫設定。不要針對下載結果臨時計算摘要,再用它驗證同一份檔案;那只能證明檔案在兩次讀取之間沒有變化,無法證明來源正確。
cd /opt/build-tools
shasum -a 256 -c example-tool.sha256
codesign --verify --deep --strict --verbose=2 example-tool
內部自行編譯的工具可以沒有對外散布用的簽章,但至少應保留提交版本、建置指令碼版本與預期摘要。如果工具以壓縮檔提供,應先驗證壓縮檔,再解壓縮至暫存目錄,檢查最終可執行檔,最後以不可分割操作取代正式路徑。如此一來,runner 就不會在更新途中讀到不完整的檔案。
只清除目標檔案的隔離屬性
確認摘要、版本與簽章都符合預期後,只處理已驗證的目標。先讀取原始值並寫入作業日誌,再刪除指定屬性。
TOOL="/opt/build-tools/example-tool"
xattr -p com.apple.quarantine "$TOOL" 2>/dev/null || true
if xattr -p com.apple.quarantine "$TOOL" >/dev/null 2>&1; then
xattr -d com.apple.quarantine "$TOOL"
fi
spctl --assess --type execute --verbose=4 "$TOOL"
"$TOOL" --version
不要對工作區執行 xattr -cr .。這會同時清除儲存庫、相依套件、指令碼與暫存產物的多種延伸屬性,不但擴大放行範圍,也會讓後續調查失去來源證據。應用程式套件確實可能需要處理套件內繼承的屬性,但範圍仍應限定在已驗證的單一套件,而不是 runner 的整個快取根目錄。
將檢查固化為安裝門禁
穩定的雲端 Mac CI 不應在每個建置任務中臨時修正權限。應將工具準備流程拆成獨立的安裝階段:下載至暫存目錄、核對摘要、檢查架構、驗證簽章、讀取隔離狀態、定點放行、執行版本自我檢查,最後才寫入共用工具目錄。
快取鍵至少應包含工具名稱、版本、CPU 架構與摘要。即使命中快取,仍須執行輕量檢查,因為快取可能遭到手動覆寫。runner 啟動時可以記錄 id -un、uname -m、工具解析後的路徑與版本,但不要輸出認證資訊或完整的環境變數。
最後請保留三項失敗處理原則:摘要不符時立即停止;原本應有簽章卻驗證失敗時重新取得產物;只有隔離評估阻擋了已驗證的產物時,才刪除目標屬性。這種做法雖然比全域關閉檢查多了幾道命令,卻能讓工具來源、放行操作與實際執行版本都可供稽核。
常見問題
為什麼工具已有執行權限,仍會遭 macOS 阻擋?
執行權限只控制檔案能否被啟動。Gatekeeper 還會檢查隔離屬性、程式碼簽章、來源與系統安全政策,因此 chmod +x 無法處理所有阻擋情況。
可以對整個 CI 工作目錄遞迴執行 xattr -cr 嗎?
不建議。這會移除目錄內所有檔案的擴充屬性並掩蓋未知產物的來源。應先驗證雜湊與簽章,再只刪除目標檔案的 com.apple.quarantine。
如何避免同一工具在每次建置時重複遭阻擋?
把下載、雜湊驗證、解壓縮與定點放行放入受控的工具安裝階段,並以工具版本、架構及雜湊作為已驗收檔案的快取鍵。
在獨享 Apple Silicon 實體節點上執行建置
依機型、區域與租用週期設定節點,實際可用狀態以控制台即時回傳結果為準。