症状:同一台 Mac 同时保存源码副本、Apple 签名私钥、部署令牌,还能访问内部网络,但 Runner 页面仍显示 Online。
本周建议:先禁止公开仓库和不受信任 Pull Request 使用这台节点;随后建立独立 Runner Group,拆分构建与发布节点,并用“任务后清理、重启恢复、异常停派”三项证据验收。无法彻底重建的共享 Mac,只运行低权限构建。
这篇文章适合需要在 GitHub Actions 中运行 Xcode 构建与 Simulator 测试的移动开发团队,也适合负责 Runner Group、仓库权限、网络边界和节点运维的 DevOps、平台工程师,以及管理 Apple 签名证书、macOS Keychain 和生产发布审批的安全负责人。
先划清边界:私有仓库不等于可信 Runner
GitHub Actions 自托管 Mac Runner 安全的第一判断,不是看仓库名称是否为 Private,而是看谁能修改工作流、触发任务,以及任务能控制哪台宿主机。
GitHub 官方明确说明,自托管 Runner 不具备托管 Runner 那种临时、干净虚拟机的持久化隔离保证。不受信任的工作流代码可能修改宿主机,并把影响留给后续任务。公开仓库尤其危险,因为外部贡献者可以通过 Pull Request 触发执行。(docs.github.com)
在上线前,先把节点归入以下三类:
- ❌ 禁止接入:公开仓库、不受信任 Pull Request、可被大量协作者修改的工作流。
- ⚠️ 隔离试跑:私有仓库,但触发者范围、网络出口或任务后清理还没有证据。
- ✅ 允许上线:受控私有仓库、专用 Runner Group、低权限令牌、独立签名节点,并能完成清理和重建验收。
核实对象包括仓库可见性、分支保护、工作流文件写入权限、Pull Request 审批人、手动触发权限和环境批准人。可观察证据应是一次真实的“允许任务”和一次真实的“拒绝任务”,而不是设置页面截图。
提醒: Runner 显示 Online 只代表它能接收任务,不代表工作区、进程、Keychain 或网络状态可信。安全结论必须来自任务后的检查和重启后的复测。
Runner Group 与标签:从“能运行”改成“只能被指定任务使用”
Runner Group 是第一道路由边界。GitHub 支持按组织或企业管理 Runner Group,并通过仓库访问策略限制哪些仓库可以使用其中的 Runner。新建 Runner 默认可能进入默认组,因此不能假设注册完成后就已经完成隔离。(docs.github.com)
核查时记录以下对象:
- Runner 注册层级:仓库、组织还是企业。
- Runner 所属 Group:是否仍在默认组。
- 仓库白名单:是否只包含占位符仓库。
- 工作流触发条件:是否包含
pull_request、workflow_dispatch或不受控分支。 runs-on条件:是否同时指定 Group 和多个标签。- 标签命名:是否把标签误当成权限控制。
GitHub 文档说明,工作流可以通过 Group 和标签共同选择 Runner;组合条件必须同时满足。标签适合表达系统类型、芯片架构或用途,但不能单独承担信任边界。真正的访问限制来自 Group 的仓库策略和工作流权限。(docs.github.com)
建议验收一组最小对照:
runs-on:
group: mac-build-group
labels:
- self-hosted
- macos
- build
允许任务应来自白名单仓库,并落到预期节点;拒绝任务应来自未授权仓库、错误 Group 或不满足标签的工作流。两者都要保存运行结果、时间、仓库标识和 Runner 名称。
若发现默认组仍可被其他仓库使用,停止派发签名任务。若只能依赖一个容易复制或误用的标签,而没有仓库白名单,也只能进入隔离试跑。
凭据分层:构建节点不应同时拥有发布能力
GitHub Actions、self-hosted runner 和 macOS Keychain 的权限边界必须分开处理。一个工作流拥有某个 Secret,不代表宿主机就安全;一个环境设置了人工审批,也不代表宿主机已经隔离。
GitHub 官方指出,环境 Secret 只有在引用该环境且通过必要审批后才会提供给任务,但使用自托管 Runner 时,工作流并不会因此运行在隔离容器中。环境审批能限制发布流程,却不能修复已经被不受信任代码控制的 Mac。(docs.github.com)
我们建议把资产拆成四层:
- 普通构建令牌:只读源码、下载依赖或上传测试产物。
- 仓库部署密钥:限定单仓库、单环境和最小操作范围。
- Apple 签名身份:包含证书与私钥,只存在发布 Runner 的专用 Keychain。
- 生产发布权限:独立环境、独立审批人、独立任务,不与普通构建共用。
Apple 文档说明,代码签名身份由证书和私钥共同构成,私钥通常保存在 Mac 的登录 Keychain 中;只有公钥证书本身不能完成签名。(developer.apple.com)
因此,验收对象不是“证书文件是否加密”,而是执行账户能否访问私钥、哪些进程被授权使用 Keychain、工作流是否能读取环境变量,以及任务参数是否把密钥暴露给进程列表或日志。GitHub 也提醒,日志脱敏不是绝对安全边界,任务仍可能主动把秘密变形后发送出去。(docs.github.com)
满足以下条件时,才允许签名节点上线:
- 只接受受控仓库和受控发布工作流;
- 构建节点无法读取发布 Runner 的 Keychain;
GITHUB_TOKEN使用最小权限;- 发布环境有独立审批;
- 证书、私钥、部署令牌可以单独轮换。
如果普通 Pull Request 能运行在安装了 Apple Distribution 身份的节点上,应立即停止派单并迁移签名资产。
六项隔离指标的上线判断
下面这张表用于区分“可以上线”“只能试跑”和“必须回退”。它不是配置推荐,而是验收结论工具。
| 安全指标 | 核实对象 | 通过证据 | 可接受结论 | 停止条件 |
|---|---|---|---|---|
| 信任来源 | 仓库、触发者、工作流修改权 | 未授权 Pull Request 无法派单 | 受控私有仓库上线 | 公开仓库可执行任意任务 |
| 任务路由 | Runner Group、仓库白名单、标签 | 允许与拒绝任务结果相反且明确 | 专用 Group 上线 | 默认组仍开放给其他仓库 |
| 凭据边界 | GITHUB_TOKEN、Secret、Keychain |
构建任务无法读取发布身份 | 构建与发布分节点 | 普通任务可访问签名私钥 |
| 任务残留 | 工作区、缓存、进程、配置 | 清理后无残留,重启后状态一致 | 可复用节点 | 出现未知进程或 Keychain 痕迹 |
| 网络权限 | 内部服务、包仓库、发布接口 | 允许目标可达,禁止目标被拒绝 | 最小网络范围 | 可横向访问管理网络 |
| 恢复审计 | 日志、轮换、重建、备用节点 | 异常可停派并恢复 | 具备生产准入证据 | 只能人工猜测节点是否干净 |
清理工作区不等于清理宿主机
多人共享 Mac Runner 时,最容易被低估的是任务结束后的持久化。删除仓库目录,只能处理一部分源码副本,不能覆盖 DerivedData、Xcode 归档、依赖缓存、用户级配置、临时文件、后台进程和 Keychain 授权。
每次任务结束后,至少执行以下检查:
- ✅ 仓库副本、构建归档和导出包已删除;
- ✅
DerivedData、依赖缓存和临时目录符合保留策略; - ✅ 没有残留的 Shell、编译器、模拟器或下载进程;
- ✅ 没有新增 LaunchAgent、登录项或用户级配置;
- ✅ 环境变量写入文件、日志和调试转储已检查;
- ✅ SSH 密钥、部署令牌和临时证书没有落盘;
- ✅ Keychain 没有新增身份、授权条目或异常访问记录。
GitHub 建议使用一次性或临时 Runner 来减少前一任务影响后续任务的机会,但官方同时提醒,复用硬件时仍需自动化保证环境干净。JIT 注册只控制 Runner 的生命周期,不等于宿主 Mac 已被彻底清理。(docs.github.com)
经验: 如果节点无法自动重建,且团队没有办法解释某个常驻进程、Keychain 条目或网络连接的来源,就不要继续派发签名、发布和内部系统访问任务。先停派,再人工复核或重置。
网络与主机权限:把横向影响限制在任务所需范围
Mac Runner 的风险不仅来自源码。还来自它能访问什么。很多 Xcode CI 节点同时连接 GitHub、私有包仓库、测试 API、发布接口、内部数据库或管理网络。一旦工作流代码获得宿主机控制权,网络边界就决定了影响范围。
GitHub 的自托管 Runner 通常需要向外建立 HTTPS 连接,官方参考中列出 70 kilobits per second 的上下行最低通信要求,以及通过 443 端口进行出站连接的要求。实际任务还可能需要额外访问包仓库、制品服务或团队内部 API。(docs.github.com)
核查对象应包括:
- Runner 是否需要访问内部管理网;
- 是否可以访问云元数据服务;
- 是否能直接连接生产 API;
- 日常构建是否真的需要
root; - 是否启用了完整磁盘访问;
- 是否允许无限制外网;
- 签名和发布步骤是否能单独使用更窄的出口。
通过条件不是“网络完全断开”,而是建立明确的允许与拒绝清单。例如,允许访问 GitHub、指定包仓库和测试服务;拒绝访问生产数据库、管理接口、未使用的内网网段和不必要的云控制面。
如果某个构建步骤需要高权限,应拆成独立任务,并把它放在专用节点和独立审批之后。若普通测试任务也能以管理员账户运行,或者节点拥有不必要的完整磁盘权限,应回退到低权限构建。
从低权限试跑到生产准入
GitHub Actions 自托管 Mac Runner 安全不能靠一次配置完成。我们建议按以下步骤落地:
1.盘点谁能让任务运行
列出仓库管理员、工作流写入者、Pull Request 触发者、手动运行者和环境审批人。对每个角色记录“能修改什么”和“能访问哪个 Runner”。
2.建立信任分级
把公开仓库、不受信任 Pull Request、受控私有仓库和发布工作流分别标记。公开代码不进入带有签名资产或内部网络权限的节点。
3.重建 Runner Group
创建专用 Group,移出默认组,配置仓库白名单,并在工作流中同时使用 Group 与用途标签。保存允许任务和拒绝任务的结果。
4.拆分构建与发布节点
构建节点只负责源码编译、测试和产物生成。发布节点才接触 Apple 签名身份、发布令牌和生产接口。两者使用不同账户、不同 Keychain 和不同网络策略。
5.限制令牌与环境审批
将 GITHUB_TOKEN 设为最小权限。仓库部署密钥限定仓库范围。发布 Secret 放入独立环境,并要求指定审批人。注意,审批只控制任务是否继续,不会替代宿主机隔离。
6.执行残留检查
模拟一次受控任务,制造临时文件、后台进程和构建缓存,再执行清理。检查工作区、进程、配置、Keychain 和网络连接,而不是只看目录是否为空。
7.验证重启与恢复
重启节点后重新检查 Runner 配置、账户、Keychain、网络规则和工作流状态。若节点无法恢复到已知状态,停止派发敏感任务,执行重置或换用一次性重建环境。
8.完成四类复测
至少覆盖低权限构建、受控签名、恶意残留模拟和节点重启。GitHub 官方建议临时 Runner 只处理单个任务;如果使用长期节点,则必须额外证明清理和恢复流程有效。(docs.github.com)
若团队还没有完成 Mac 节点的重启验收,可先参考我们的 Mac mini 远程租赁方案,重点核对账户隔离、远程访问方式和节点恢复路径,而不是先把生产签名密钥迁移过去。
按风险选择最终方案
用下面的条件分支做生产决策:
- 若仓库公开,或 Pull Request 来源无法确认:不使用自托管 Mac Runner,回退到托管执行环境或无敏感资产的隔离试跑节点。
- 若仓库私有,但多个仓库共享同一默认 Group:先重建 Runner Group 和仓库白名单,不允许签名任务上线。
- 若构建与发布共用账户、Keychain 或网络出口:拆分节点;在拆分完成前只运行低权限测试。
- 若任务后能清理工作区,但不能确认进程、配置和 Keychain 状态:把长期节点视为不可复用,转入人工复核或一次性重建。
- 若节点可自动重建、发布环境有审批、网络有允许与拒绝证据:可以先迁移非生产仓库,再逐步引入受控签名任务。
- 若团队需要物理 USB、专用硬件或长期高负载且无法接受远程恢复流程:优先购买并自行维护专用 Mac,不要把租赁节点当成万能替代。
当前使用共享 Mac、普通云主机或临时拼接的 CI 方案,常见缺点是账户边界不清、签名资产和构建缓存混在一起、节点污染后难以证明已经恢复。相比之下,使用 MESHLAUNCH 的远程 Mac 做隔离试跑,可以先验证真实 macOS 环境、独立账户、重启恢复和节点重置,再决定是否迁入生产任务;但签名私钥、内部网络和发布审批仍应由团队自己定义边界。需要了解可用节点时,可从 MESHLAUNCH 的远程 Mac 入口开始,先运行非生产仓库并保留完整验收证据。