For open-source maintainers, iOS security leads, and platform engineers, the key decision is whether an external PR can reach a persistent Mac runner or sensitive credentials. This guide checks workflow trust, runner access, secrets, cleanup, and recovery, then provides pass-or-fail conditions for routing untrusted work.
Decision: Do not run an untrusted external PR directly on a long-lived self-hosted Mac runner that holds signing credentials or reusable state. Separate untrusted validation from trusted release work, restrict runner-group access, remove sensitive credentials from PR jobs, and verify cleanup before the host is reused. If you cannot prove those controls, send the PR to a separate isolated environment instead.
This guide is for open-source maintainers deciding whether external contributions can reach a self-hosted runner. It also helps iOS security leads separate test builds from signing and release jobs. Platform engineers can use it to check repository access, cleanup, and node recovery.
The trust boundary breaks at workflow execution
A runner executes the workflow that GitHub Actions dispatches; it is not merely a neutral machine that compiles source code. A pull request can change files that affect the workflow, build scripts, dependencies, and commands the job runs. GitHub’s secure-use guidance for Actions specifically warns about the risks of using self-hosted runners with untrusted code.
That warning matters even when the workflow file looks familiar. A malicious change can target a tool invoked by the workflow, a dependency installation step, or data left by an earlier job. A persistent Mac adds a second concern: the risk may continue after the PR job ends if files, processes, or credentials remain available to later work.
Start by classifying the event and the code it causes to run. GitHub’s workflow trigger reference distinguishes event behavior; review the actual trigger, checkout reference, and permissions rather than assuming every pull request runs under the same trust conditions.
| Workflow source | Initial decision | What to verify |
|---|---|---|
| Protected branch maintained by trusted staff | May use a trusted runner if release credentials and job permissions are appropriate | Branch protections, review rules, workflow changes, and runner access |
| Pull request from inside the organization | Treat according to the contributor’s actual trust and repository permissions | Whether the author can modify workflow code, access secrets, or reach a shared runner |
| External contributor’s pull request | Keep off a sensitive or persistent runner unless isolation is demonstrated | Runner group access, credentials, host state, and cleanup evidence |
pull_request_target workflow | Do not use it to execute untrusted PR code with privileged context | Follow GitHub’s specific pull_request_target security guidance |
The last row deserves careful review. pull_request_target can be useful for limited tasks that need the base repository’s context, but that does not make checking out and executing a contributor’s code safe. GitHub’s documentation explains the security risks and cautions against combining privileged context with untrusted code. Keep metadata handling and code execution separate.
A workflow’s name, trigger label, or “test” purpose does not establish that its code is trusted.
Runner access controls must block unauthorized routing
A runner group is the access boundary to inspect. Labels are useful for selecting a runner with a capability, such as a macOS environment, but a label by itself does not restrict which repositories may use that runner. GitHub describes runner groups and their access model, and its runner access documentation covers controlling which repositories can access self-hosted runners.
| Control | What it does | Acceptance test |
|---|---|---|
| Runner-group repository policy | Limits which repositories can access a group | A repository outside the allowed set cannot route a job to it |
| Runner labels | Help workflows select a compatible runner | Treat as routing metadata, not proof of authorization |
| Organization-level policy | Sets access rules across repositories | Check how the policy interacts with group and repository settings |
| Workflow placement | Determines which jobs request the runner | Confirm untrusted PR workflows do not request a sensitive group |
Inspect both sides of the route: the group policy and the workflow’s runs-on configuration. A job requesting the intended label has not passed an access review if the group still allows repositories that should not use it. Likewise, restricting repository access does not make an unsafe workflow safe for every user who can submit code to an allowed repository.
For each runner group, record which repositories are permitted, which teams own the policy, and which workflows are intended to use it. Then test from a repository that should not have access. The test should fail at the access boundary, not merely remain unscheduled because a matching runner happens to be offline.
A useful acceptance rating is simple: Pass means the group policy blocks unauthorized repositories and the workflow routes only approved jobs. Fail means access depends on labels alone, the allowed repository list is unclear, or the negative test succeeds. This is a triage rubric, not a security certification.
PR tests must stay separate from signing credentials
Treat credentials as a separate boundary from runner access. A job may be unable to publish a release but still be able to read a signing secret or use a token with broader permissions than the test requires. GitHub’s documentation on using secrets in workflows explains how secrets are made available to workflows. Review the actual job and event context; do not infer safety from a secret being absent in the visible log.
| Job type | Signing or publishing secrets | Recommended gate |
|---|---|---|
| External PR validation | Do not provide them | Build and test with non-sensitive inputs |
| Trusted merge validation | Provide only if the job has a documented need | Review who can change the workflow and what permissions it receives |
| Release or signing job | Keep separate from untrusted PR execution | Require a trusted release path and narrowly scoped credentials |
GitHub’s secure-use documentation also describes the permissions available to workflows and the risks of overly broad access. Use the minimum permissions needed for each job. Separate PR validation from signing and publishing rather than relying on a condition buried in a shared workflow to protect credentials.
Do not use “the secret was not printed” as the test result. Code can read a credential without echoing it, pass it to another process, or use it to perform an action. Instead, verify whether the PR job receives the secret at all, whether its token permissions are limited, and whether the job can reach systems that accept the credential.
External-PR isolation review criteria
The checks below translate the risk into a decision. Use them before routing external work to a Mac runner, and repeat them after changes to workflow permissions, runner groups, or host setup.
- Source trust — Pass if the workflow’s code and checkout target are understood and the job is treated according to the contributor’s trust level. Fail if the team assumes a familiar workflow file makes a fork’s code safe.
- Runner authorization — Pass if repository access is controlled by the runner group and a negative access test is blocked. Fail if the decision relies on a label or an undocumented organization setting.
- Credential separation — Pass if external PR jobs cannot access signing, publishing, or unrelated sensitive credentials. Fail if credentials are available and the only assurance is that logs look clean.
- Host reuse — Pass if post-job cleanup is verified and no prior task state is available to the next workload. Fail if the runner is reused without checking its workspace, temporary files, caches, and processes.
- Recovery — Pass if the team can remove the runner from service, restore it, and review the relevant logs. Fail if nobody owns that decision or if the node returns to trusted work without a documented check.
Routing decision: If every critical check passes and a real test confirms the boundary, use only the runner group approved for that untrusted workload. If any check fails or cannot be verified, do not route the external PR to that runner. Use an isolated environment for the PR and reserve the sensitive Mac runner for trusted jobs.
FAQ
Can an external PR use a self-hosted Mac runner?
Only route it there if the runner is isolated from sensitive credentials, trusted workloads, and reusable host state, and if you have verified those controls in practice. Otherwise, keep the PR on a hosted or separately isolated environment. GitHub warns that self-hosted runners can expose the host and its environment to untrusted workflow code.
How do I restrict which repositories can use a self-hosted runner?
Use runner groups to define repository access at the organization level, and review the group’s repository policy as well as workflow routing. Labels help a workflow select a runner, but a label is not an authorization boundary. Confirm that an unauthorized repository cannot use the group; do not treat a matching label as proof of access control.
How can I keep signing credentials away from PR tests on a Mac runner?
Separate PR validation from release workflows, and do not expose signing secrets or publishing credentials to jobs that execute untrusted code. Review both workflow permissions and secret scope. A secret that does not appear in logs may still have been read or used by a process, so log inspection alone cannot establish isolation.
What should I verify after an external PR finishes on a self-hosted runner?
Check that the working directory, temporary files, relevant caches, and processes from the job are gone before allowing another workload onto the host. Then run a non-sensitive test workflow to verify the cleanup behavior. If you cannot prove the runner is clean, keep it out of trusted jobs and restore or rebuild it under your recovery procedure.
Prove cleanup before recovering a runner
A cleanup setting in YAML is not evidence that a reused host is clean. The runner may have state outside the checked-out workspace, including temporary files, caches, or processes started by a build script. Review the actual runner installation and job lifecycle. GitHub documents scripts that run before or after a self-hosted runner job; these can support cleanup, but the existence of a script does not prove it succeeded or removed every relevant artifact.
Use a non-sensitive test workflow to check the behavior end to end:
- Prepare a test job that creates a uniquely identifiable file in the workspace and a temporary location, then starts a harmless background process.
- Let the job finish through the normal runner lifecycle. Do not inspect only the workflow source or cleanup script.
- Before assigning another job, verify that the test file and process are absent and that the next job cannot read the earlier workspace state.
- Check the relevant runner and job logs for cleanup errors. Record who reviews failures and who is authorized to return a node to service.
- If any residue remains, remove the runner from the trusted pool. Restore it using the team’s approved recovery method, then repeat the test before reuse.
The test should avoid real keys, release assets, customer data, and production endpoints. Its purpose is to prove that the cleanup path behaves as expected, not to simulate an attacker with sensitive access.
Long-lived runners also need a clear response when the team cannot establish whether a job left state behind. Keep the node out of trusted release work while it is being investigated. Preserve the logs needed for review, identify the responsible workflow, and document whether the recovery involved cleaning or rebuilding the host. A successful test is evidence about that test run; it is not a permanent guarantee after the runner image, scripts, or workflows change.
What to do when a gate fails
Use the failure to make a routing decision, not to create an exception with no owner.
- If a repository outside the intended group can use the runner, restrict group access and retest before routing PR work.
- If a PR job can access signing or publishing credentials, remove that access and separate the release workflow. Keep the job off the sensitive runner until the change is verified.
- If cleanup leaves files or processes behind, stop reusing the node for trusted work. Restore it, then run the non-sensitive test again.
- If the team cannot determine which workflow or repository should be trusted, default to an isolated environment and ask the repository or platform owner to define the boundary.
- Record the decision, the evidence reviewed, and the person responsible for restoring the node. Revisit the decision when the runner group, workflow, permissions, or cleanup process changes.
These checks address the specific risk of untrusted code reaching a self-hosted macOS CI node. They do not establish compliance with a regulation or security certification, and they do not mean GitHub or a Mac provider automatically handles isolation or cleanup for you.
A shared, long-lived runner can be convenient, but its broad reuse, leftover state, and proximity to signing credentials make external PRs harder to contain. A Mac environment you provision for CI can make it easier to separate test work from trusted release work, but renting hardware does not itself provide isolation; you still need to configure access, secrets, and cleanup. Review the available Mac VPS options against your actual runner design, and contact ZekVPS support to clarify environment details before relying on them.
Give Trusted macOS Builds a Dedicated ZekVPS Mac
Keep untrusted pull-request jobs away from persistent machines, and run trusted macOS CI on a bare-metal Mac mini dedicated to you.
Separate project environments by running them on individual ZekVPS instances.
If you are moving MCP or Agents from demo to daily use, a snapshot-ready cloud Mac node beats swapping frameworks again. View ZekVPS cloud Mac mini plans — Separate lab from daily driver for calmer deployments.