凌晨两点,团队里第三个人在同一台云端 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 凭据,配置随时可升级。