凌晨兩點,團隊裡第三個人在同一台雲端 Mac 上跑了一次 fastlane build,結果 CI 直接報錯:證書失效。三個人各自在本地跑過 match,誰也沒注意到自己那一次操作,已經把別人剛匯入的簽名證書從系統預設 Keychain 裡擠了出去。這種事故在多人共用一台雲端 Mac 出包時幾乎是必經的坑——本文記錄我們如何用 fastlane match 把證書統一管起來,並解決無頭遠端連線裡 Keychain 反覆要密碼的麻煩。
問題:共用一台雲端 Mac,證書為什麼總出岔子
本機開發時,一台 Mac 通常只服務一個人,證書裝進預設 Keychain 誰都不會打架。但一台按天租來的雲端 Mac 常常是團隊共用:今天你連上去打包,明天同事遠端登入改個設定又跑一次。如果每個人都各自執行 fastlane match development 或手動匯入 p12,系統預設 Keychain 裡的證書條目就會被反覆覆蓋、刪除又重建,最終出現「證書存在但簽名時找不到私鑰」這種典型報錯。
根本原因是兩件事被混在一起了:證書的儲存與分發,和證書在本機 Keychain 裡的暫時授權。前者應該集中管理,後者才需要按會話隔離。
fastlane match 的核心思路
match 把開發證書、發行證書和對應的描述檔加密後統一存進一個獨立的 Git 儲存庫,團隊所有人(以及雲端 Mac 上的自動化使用者)都從這個儲存庫拉取同一份證書,而不是各自去 Apple Developer 後台重新產生。這樣能保證同一個 Bundle ID 在任何機器上簽名結果一致,也避免了「證書數量超過 Apple 限額」的老問題。
match 的價值不是「自動產生證書」,而是「不再需要每個人自己產生證書」——這一句話決定了它該怎麼被使用:只應該有一個人(或一次 CI 初始化任務)執行寫入操作,其餘所有場景都用唯讀模式拉取。
在雲端 Mac 上初始化證書儲存庫
在你的雲端 Mac 專案目錄下,先確認 Matchfile 指向私有儲存庫而不是公開位址:
git_url("git@github.com:your-org/certs-private.git")
storage_mode("git")
type("appstore")
首次初始化只在一台受信任的機器上執行一次:
fastlane match appstore --readonly false
之後所有雲端 Mac 上的拉取操作都加 --readonly true,確保它們只讀取既有證書、不會嘗試重新產生:
fastlane match appstore --readonly true
給部署使用者單獨發一個唯讀的 Git 部署金鑰(deploy key),而不是把團隊成員的個人 SSH key 都塞進雲端 Mac,權限邊界更清楚,人員異動時也不用逐個撤銷。
無頭遠端連線裡的 Keychain 免密解鎖
雲端 Mac 大多是透過 SSH 或 VNC 遠端登入,不是本人親自坐在螢幕前打密碼,系統預設的登入 Keychain 在連線之間經常處於鎖定狀態,codesign 一執行就跳出授權對話框——這在無人值守的 CI 觸發場景裡會直接卡死。
做法是新建一個專用的簽名 Keychain,和登入 Keychain 分開:
security create-keychain -p "$KEYCHAIN_PWD" signing.keychain
security set-keychain-settings -lut 21600 signing.keychain
security unlock-keychain -p "$KEYCHAIN_PWD" signing.keychain
security import cert.p12 -k signing.keychain -P "$P12_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PWD" signing.keychain
list=$(security list-keychains -d user | tr -d '"')
security list-keychains -d user -s signing.keychain $list
其中 set-key-partition-list 這一步很容易漏掉,漏了的話 codesign 依然會跳窗要求授權,即便 Keychain 已經解鎖。這台專用 Keychain 建議只保留簽名需要的證書,不要和日常密碼混放,退租或轉交專案時刪掉它比清理預設 Keychain 乾淨得多。
CI 觸發後的證書拉取與清理
自動化流程建議按下面的順序執行,每一步的失敗都應該讓流水線直接停下,而不是繼續往後跑:
| 步驟 | 命令/動作 | 失敗時的處理 |
|---|---|---|
| 解鎖專用 Keychain | security unlock-keychain |
立即中止,報工單而非靜默重試 |
| 唯讀拉取證書 | fastlane match … --readonly true |
中止,提示先在主控機執行寫入 |
| 執行建置 | xcodebuild archive |
保留建置日誌供排查 |
| 清理暫時 Keychain 參照 | security delete-keychain signing.keychain(按需) |
記錄清理是否成功 |
如果雲端 Mac 是按天租用、用完就會轉手給其他任務或到期回收,最後一步的清理尤其重要:專用簽名 Keychain、暫時下載的 p12、~/Library/MobileDevice/Provisioning Profiles 裡的描述檔都建議在任務結束時一併清空,不要指望下一個租期自動繼承乾淨環境。具體機型與節點是否滿足你團隊並發出包的需求,建議在控制台確認目前可選配置後再排期,不同晶片型號的編譯速度差異會直接影響多人排隊等待時長。
團隊協作與權限邊界
把職責拆清楚能省掉後續很多故障排查時間:
- 證書寫入權限:只交給一個人或一條初始化流水線,日常成員和雲端 Mac 上的自動化帳號統一用
--readonly true。 - 儲存庫存取權限:證書儲存庫用部署金鑰而不是個人帳號金鑰,方便按人按機器單獨撤銷。
- 本機 Keychain 隔離:每個使用者或每條平行建置線路分配獨立的 Keychain 檔案和獨立的建置目錄,避免互相覆蓋預設 Keychain。
- 密碼與 token 存放:
KEYCHAIN_PWD、P12_PWD、Git 部署金鑰都透過環境變數或 CI 的加密變數注入,不寫進腳本檔案本身。
上線前檢查清單
- [ ]
Matchfile指向的是私有儲存庫,團隊裡沒有人還在公開儲存庫存放過證書歷史; - [ ] 雲端 Mac 上執行的所有 match 命令都帶
--readonly true; - [ ] 專用簽名 Keychain 已執行
set-key-partition-list,確認codesign不再跳窗; - [ ] 每個平行使用者/建置線路有獨立的 Keychain 檔案和建置目錄;
- [ ] 任務或租期結束前有清理腳本刪除專用 Keychain 與暫時描述檔。
把證書管理和 Keychain 解鎖這兩件事分開處理之後,團隊在同一台雲端 Mac 上交替打包基本不會再互相踩坑,新人加入專案時也只需要拉一次儲存庫、跑一遍唯讀命令,不用重新申請證書。
常見問題
match 的憑證庫能直接用公開的 Git 儲存庫嗎?
不建議。match 會把 p12 憑證與描述檔加密後存進儲存庫,雖然加了密碼保護,但公開儲存庫仍會暴露結構與元資料,應使用私有儲存庫,並給雲端 Mac 上的部署帳號單獨發只讀 token。
雲端 Mac 重新啟動後 Keychain 密碼還需要每次手動輸入嗎?
不需要。用 security create-keychain 搭配固定密碼建立專用簽名 Keychain,再用 security set-keychain-settings 關閉自動鎖定,配合 -A 授權 codesign 免確認存取,重新登入連線後執行一次解鎖腳本即可。
多人同時用同一台雲端 Mac 打包,憑證會互相覆蓋嗎?
會,如果各自跑 match,預設 Keychain 裡的憑證會被反覆匯入匯出導致衝突。做法是替每個人分配獨立建置目錄與專用 Keychain 檔,match 的 readonly 模式只讀取儲存庫憑證而不重新產生,避免互相覆蓋。
HireVPS
今天就試試獨享雲端 Mac mini
按日起租,2 分鐘取得 SSH/VNC 憑證,配置隨時可升級。