Fastlane Match Remote Mac Signing: 2026 CI Guide
A local archive succeeds, but the same lane fails on the remote Mac with “no signing certificate,” “profile not found,” or a hanging keychain prompt.
Winner: readonly fastlane match plus an isolated keychain, explicit target mappings, post-build verification, and cleanup. A successful build_app call alone is not enough. The node must also pass restart, concurrency, certificate rotation, and recovery tests.
This guide is for:
- DevOps engineers maintaining iOS or macOS release pipelines.
- Release engineers sharing one Mac node across apps, extensions, or Apple Teams.
- Independent developers moving a local fastlane workflow to a long-running remote Mac.
Why local signing does not prove remote CI readiness
The usual failure is not the compile command. It is an incomplete migration of signing state.
A working local build depends on four separate boundaries:
- The signing certificate.
- The matching private key.
- The provisioning profile for the exact Bundle ID.
- The Xcode project and export settings that select those assets.
Apple states that a certificate without its corresponding private key cannot be used for code signing. The private key normally remains in the originating Mac’s keychain, so copying only a certificate or profile to a remote node creates an incomplete signing identity. Apple’s certificate synchronization documentation explains this relationship.
A second boundary is the keychain session. An interactive SSH session may unlock a keychain successfully while a background runner cannot access the same private key. The runner may also select an old login keychain, an expired identity, or a keychain left by a previous job.
A third boundary is profile selection. Xcode’s DEVELOPMENT_TEAM and PROVISIONING_PROFILE_SPECIFIER settings determine which team and profile are used. Apple documents that a missing or invalid profile causes a build error, and that the profile setting can contain a profile name or UUID. Apple’s build settings reference covers these settings.
A fourth boundary is export. An archive can succeed while export fails because the distribution profile does not match the archive’s Bundle ID, entitlement set, or export method.
The safe migration rule is simple:
Treat certificates, private keys, profiles, keychains, project settings, and export settings as separate testable inputs. Do not use one green lane result as proof that all six are correct.
The minimum remote Mac design for one App
For a single App Store build, the smallest reliable design has three responsibilities:
setup_ciprepares a temporary keychain and switchesmatchtoward CI-safe behavior.matchretrieves the existing signing assets.build_apparchives and exports the application.
The current fastlane documentation describes setup_ci as creating a temporary keychain, switching match to readonly, and preparing log and test-result paths. Its documented default keychain timeout is 3,600 seconds, with a configurable timeout and keychain name. The official setup_ci action documentation should be checked again whenever the installed fastlane version changes.
A minimal lane can look like this:
platform :ios do
lane :release do
setup_ci(
provider: ENV["CI_PROVIDER"],
timeout: 0,
keychain_name: "fastlane_ci_keychain"
)
match(
type: "appstore",
app_identifier: "com.example.app",
readonly: true
)
build_app(
scheme: "App",
clean: true
)
end
end
The values above are placeholders. The repository URL, Bundle ID, scheme, provider, and secrets must be supplied through the project’s own configuration. Never commit a real MATCH_PASSWORD, repository token, App Store Connect key, private key password, or SSH credential.
The official match documentation states that signing files can be stored in Git, Google Cloud Storage, or Amazon S3. For Git storage, certificates and private keys are encrypted with a passphrase. The same documentation recommends readonly mode on CI so a build job does not create or revoke signing assets unexpectedly. The fastlane match documentation is the source for the current storage and parameter behavior.
Should a remote Mac CI job use readonly mode? Yes, when the job’s purpose is repeatable building and publishing rather than certificate administration. Profile creation and certificate rotation should happen in a controlled maintenance workflow, not inside an ordinary pull-request or release job.
A Matchfile can hold stable repository settings:
git_url("git@code-host.example:mobile-signing.git")
git_branch("team-production")
app_identifier([
"com.example.app"
])
username("apple-account@example.com")
The values are illustrative. The signing repository should be private, access should be limited to the required runner or team, and the encryption passphrase should be injected through the CI secret store.
Keychain isolation is the difference between a build and a runner
How should fastlane match configure the keychain on a remote Mac? It should import the signing identity into a job-specific or temporary keychain, make that keychain available to the signing process, and remove it after the job.
setup_ci is the preferred fastlane-level starting point because it combines temporary keychain creation with CI behavior. If a pipeline needs a manually managed keychain, fastlane also documents unlock_keychain, which can unlock a keychain, add it to the search list, and optionally replace the existing search list. The official unlock_keychain documentation provides the current parameter names.
Useful diagnostic commands include:
security list-keychains -d user
security default-keychain -d user
security find-identity -v -p codesigning
A healthy result should show the expected Apple Development or Apple Distribution identity and its certificate hash. It should not rely on an expired identity with a similar display name.
For a more direct keychain check:
security find-certificate -a -p "$KEYCHAIN_PATH" \
| openssl x509 -noout -subject -dates
The command should be run against the keychain actually used by the job. Checking the interactive user’s default keychain is not sufficient if the CI service runs under another account or session.
Apple’s code-signing documentation notes that codesign searches keychains differently depending on whether a keychain file is explicitly supplied. Supplying a keychain can restrict the search to that file, while omitting it allows broader keychain discovery. This is one reason implicit keychain selection can produce different results between local and CI sessions. Apple’s certificate technote explains the distinction.
A cleanup trap is leaving the temporary keychain unlocked after the job. A long-running Mac runner can then expose old signing identities to a later job. Cleanup should run on both success and failure:
cleanup_signing_state() {
security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true
rm -rf "$HOME/Library/MobileDevice/Provisioning Profiles" 2>/dev/null || true
}
trap cleanup_signing_state EXIT
The profile cleanup path must be verified for the installed Xcode and macOS version. Xcode 16 release notes document a provisioning-profile location under ~/Library/Developer/Xcode/UserData/Provisioning Profiles, so scripts should discover and verify paths rather than assuming one historical directory. Apple’s Xcode 16 release notes document this path change.
Explicit profile mapping beats implicit target selection
How should multiple Targets map to different provisioning profiles? Map every Bundle ID to its intended profile and verify the mapping in both build settings and the exported archive.
A typical project may include:
- The main App.
- A Widget Extension.
- A Notification Service Extension.
- A Share Extension.
- A watchOS companion target.
Each target can have a different Bundle ID. A profile that works for the main App does not automatically authorize an extension with another identifier. Apple describes provisioning profiles as binding the signing authority, permitted apps, runtime destinations, validity period, and entitlements. Apple’s provisioning profile technote provides the underlying model.
The fastlane side can request several identifiers:
match(
type: "appstore",
app_identifier: [
"com.example.app",
"com.example.app.widget",
"com.example.app.notifications"
],
readonly: true
)
The Xcode side should preserve the mapping in build settings or an export options file. A simplified export file may look like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>provisioningProfiles</key>
<dict>
<key>com.example.app</key>
<string>match AppStore com.example.app</string>
<key>com.example.app.widget</key>
<string>match AppStore com.example.app.widget</string>
<key>com.example.app.notifications</key>
<string>match AppStore com.example.app.notifications</string>
</dict>
</dict>
</plist>
Profile names must match the assets actually synchronized to the node. Do not copy this example unchanged.
Use Xcode’s effective settings as evidence:
xcodebuild \
-workspace Example.xcworkspace \
-scheme App \
-configuration Release \
-showBuildSettings \
| grep -E 'PRODUCT_BUNDLE_IDENTIFIER|DEVELOPMENT_TEAM|PROVISIONING_PROFILE_SPECIFIER'
Then inspect the archive:
find build/Example.xcarchive/Products/Applications \
-name embedded.mobileprovision -print
For every target, record:
- Bundle ID.
- Team ID.
- Profile name or UUID.
- Signing identity.
- Entitlements.
- Archive location.
- Export result.
This record is more valuable than a lane log that only says “Lane succeeded.”
Team and multi-App isolation on one Mac
Why can two teams interfere with each other on the same remote Mac? Because the node may reuse the same keychain, match branch, profile directory, Apple account, or environment variables across jobs.
The fastlane documentation supports separate Git branches for different development teams. Cloud storage layouts use the team ID as the top-level separation. The multi-team section of the official match documentation describes these models.
A defensible isolation scheme separates at least these values:
TEAM_A
MATCH_GIT_BRANCH=team-a
KEYCHAIN_NAME=team-a-ci.keychain-db
BUNDLE_IDS=com.team.a.app,com.team.a.widget
TEAM_B
MATCH_GIT_BRANCH=team-b
KEYCHAIN_NAME=team-b-ci.keychain-db
BUNDLE_IDS=com.team.b.app
The same physical Mac can host more than one workflow, but concurrent signing should be treated carefully. If two jobs share one login session, one default keychain, or one mutable profile directory, the result may not be reproducible.
For a self-hosted GitHub Actions runner, labels and runner groups should route jobs to an intended machine. GitHub documents that self-hosted runners need a supported operating system, network communication with GitHub, and enough resources for the workflow. It also documents that jobs remain queued when no matching idle runner is available. GitHub’s self-hosted runner reference is the authoritative source for those runner constraints.
Automatic signing still has a place. It is convenient for local development and early project setup. Release CI needs stronger control because a job should not silently create a new profile, select another Team, or alter a signing asset during a production build.
During staff changes, verify evidence rather than relying on memory:
- Which match branch or storage prefix was accessible.
- Which deploy key or token was revoked.
- Which Apple Team IDs remain configured.
- Which keychains and profile directories were removed.
- Whether the next release can still perform a readonly sync.
Long-running runners need restart and failure tests
A remote Mac that stays online for weeks has different risks from a disposable hosted runner.
Test the following sequence:
1. First build after a reboot
Restart the Mac. Do not open Xcode manually. Start the CI job through the same runner account used in production.
Expected evidence:
security find-identity -v -p codesigning
xcodebuild -version
fastlane match appstore --readonly
The job must complete without a GUI prompt, password dialog, or manual keychain unlock.
2. Consecutive builds
Run two successful jobs with the same signing inputs. Confirm that the second job does not depend on artifacts from the first job.
Compare:
- Match repository revision.
- Imported certificate hash.
- Profile UUID.
- Archive signing identity.
- Exported package verification.
3. Failed job cleanup
Force a failure after match but before export. Then inspect:
security list-keychains -d user
security find-identity -v -p codesigning
find "$HOME/Library/MobileDevice" -type f -name "*.mobileprovision"
The failed job should not leave another team’s profile or an unlocked private key available to the next build.
4. Concurrent job behavior
If concurrency is required, give each job an isolated workspace and keychain. Otherwise, serialize signing jobs at the CI level.
A single shared keychain can turn a harmless retry into a cross-project signing event. The safest default for production release workflows is one active signing job per keychain boundary.
Certificate rotation and node rebuild recovery
Why does fastlane match work locally but fail in CI after certificate rotation? The CI node may still contain the old certificate, an old profile, or a stale keychain access rule.
Use this recovery order:
- Confirm the certificate status in the Apple Developer account.
- Confirm which certificate and profile are stored in the match repository.
- Rebuild the node’s temporary keychain.
- Run readonly synchronization.
- Rebuild the archive.
- Verify the exported product.
- Record the old identity as invalid or removed.
A missing public/private key pair, an expired certificate, or a revoked certificate can all produce signing failures. The active asset state should be checked before changing project settings or deleting local files.
Verify the archive and application signature directly:
codesign --verify --deep --strict --verbose=2 \
"build/ExportPayload/App.app"
codesign -dvvv \
"build/ExportPayload/App.app" 2>&1 \
| grep -E 'Identifier|TeamIdentifier|Authority'
security cms -D -i \
"build/ExportPayload/App.app/embedded.mobileprovision" \
> /tmp/profile.plist
/usr/libexec/PlistBuddy -c "Print :Entitlements:application-identifier" \
/tmp/profile.plist
The output should show the expected Bundle ID prefix, Team ID, signing authority, and entitlement set.
Do not use destructive reset commands as a universal repair. Commands that revoke, regenerate, delete, or overwrite signing assets can affect other applications and release channels. Before such a change, export or document the current match state and confirm which production builds depend on it.
A node rebuild drill is complete only when:
- The old keychain no longer provides a valid signing identity.
- The new node can perform a readonly sync.
- The main App and every Extension archive successfully.
- Exported artifacts pass signature and profile inspection.
- A failed job leaves no reusable signing state.
- The rollback or rotation decision is recorded.
Decision matrix: choose the safer CI pattern
| Situation | Recommended pattern | Main risk | Required evidence |
|---|---|---|---|
| One App, one release lane | setup_ci + readonly match + build_app |
Missing private key | Valid identity and exported archive |
| Main App plus Extensions | Explicit Bundle ID and profile mapping | Wrong profile selected for an Extension | Build settings and archive inspection |
| Multiple Apple Teams | Separate match branches or storage prefixes | Cross-team asset reuse | Team-specific keychain and branch logs |
| Long-running remote Mac | Temporary keychain per job | Stale unlocked credentials | Reboot, consecutive-build, and cleanup tests |
| Parallel release jobs | Isolated workspaces and keychains | Keychain or profile contamination | Concurrent job isolation test |
| Certificate rotation | Controlled maintenance lane | Accidental revocation or stale assets | Rotation record and clean rebuild |
| Node replacement | Rebuild from versioned configuration | Hidden dependency on old state | Successful cold-start readonly sync |
Validation matrix before production use
| Test | Command or action | Pass condition | Failure response |
|---|---|---|---|
| Identity discovery | security find-identity -v -p codesigning |
Expected valid identity appears | Re-import through readonly match |
| Profile mapping | xcodebuild -showBuildSettings |
Every target has expected Team and profile | Fix project or export mapping |
| Archive signing | codesign --verify --deep --strict |
No verification error | Inspect identity and entitlements |
| Profile contents | security cms -D -i embedded.mobileprovision |
Bundle ID and Team ID match | Regenerate or select correct profile |
| Restart test | Reboot, then run the lane | No GUI prompt or manual unlock | Rework keychain setup |
| Consecutive builds | Run the lane twice | Same inputs produce valid artifacts | Remove hidden workspace dependency |
| Failure cleanup | Abort after sync | Temporary assets are removed | Add exit traps and cleanup |
| Certificate rotation | Replace the active asset in a controlled window | New archive validates | Re-sync and document rollback |
| Node rebuild | Provision a clean remote Mac | Cold-start readonly sync succeeds | Fix undocumented dependency |
The practical choice: remote Mac, local Mac, or Linux CI
A Linux server remains a strong choice for Linux-native builds, containers, and general backend CI. It is not a substitute when the pipeline requires Xcode, Apple code signing, iOS archives, or macOS-specific release tools.
A local Mac gives the most direct access to USB devices, local debugging, and interactive Xcode work. Its drawbacks are hardware cost, limited availability, and the need to keep the machine online and maintained.
A remote Mac is a better fit when the requirement is a real macOS CI node without purchasing another physical machine. It still needs disciplined keychain isolation, stable access, and a documented recovery process. It is less suitable when the workflow depends on physical peripherals or long-term, uninterrupted heavy workloads where buying and owning dedicated hardware is more economical.
For a temporary migration, a release week, or a staged CI redesign, the remote Mac environment options from SFTPMAC can be evaluated against the pipeline’s access, root-permission, and retention requirements. The relevant comparison is not only monthly cost. It is also whether the node can be rebuilt, restarted, inspected, and cleaned without manual intervention.
If a longer hardware comparison is needed, review the Mac mini rental pricing information only after the signing workflow has passed the validation matrix above. Price does not compensate for an unreproducible keychain or an ambiguous profile mapping.
The key decision is therefore conditional:
- Choose a remote Mac when you need real macOS and Apple tooling for a defined period, but do not want to purchase hardware.
- Choose owned hardware when the node will run continuously for a long period and the organization can handle maintenance, security, and replacement.
- Keep Linux CI for workloads that do not require Xcode or Apple signing.
Once the lane is designed around readonly synchronization, isolated keychains, explicit target mappings, and recovery tests, renting a remote Mac from SFTPMAC becomes a practical way to validate the complete workflow before committing to a permanent Mac mini server setup.