
Code signing on a remote Mac works once three things are in place: a signing certificate (with private key) in an unlocked Keychain, a matching provisioning profile on disk, and the right flags passed to xcodebuild. Drive the whole flow from the Mac's own terminal. Figment's virtual Intel macOS environment ($29/month, 2 vCPU / 8 GiB RAM / 80 GiB disk) is reached through its browser desktop console — inbound SSH to the VM is not provided — and it can sign and archive iOS builds using Xcode 26 or earlier, which Apple documents as the last Xcode line supporting Intel in Porting your macOS apps to Apple silicon. For arm64-only toolchains, Xcode 27, or heavier release builds, request a physical M-series Mac from $95/month; the exact chip, RAM and disk are set in the quote. Start at Figment Mac hosting or jump to /mac-hosting/start.
Three pieces must be in place before xcodebuild can sign a build. Apple's canonical reference for the archive and export flow is Distributing your app to registered devices.
.p12..mobileprovision) that ties your App ID, certificate, and — for Development/Ad Hoc — registered devices. Download it from the Apple Developer site. The profile is a CMS-signed file; it carries no private key.codesign can read the private key without a GUI prompt. On a headless Mac any dialog stalls the build.Pick the Figment plan by the toolchain, not by preference: virtual Intel macOS for Xcode 26 and earlier at $29/month, physical M-series from $95/month for Xcode 27, Apple-silicon-only SDKs, or longer release builds. Pricing and managed options are listed on /mac-hosting.
On the Mac where the CSR was originally created:
/Applications/Utilities/Keychain Access.app)..p12 format.Transfer the .p12 and .mobileprovision to the remote Mac through whatever path the plan exposes. On the Figment Intel VM, use its browser or an outbound CI job to download files from an authorized artifact store. The console has no file-upload feature. On a physical Mac delivered through a quote, confirm the access method when it's set up; if the plan exposes SSH on your account, scp is fine:
# Only if your rented Mac exposes SSH:
scp -i ~/.ssh/your_key ~/Desktop/AppDist.p12 user@YOUR_MAC_IP:~/signing/AppDist.p12
scp -i ~/.ssh/your_key ~/Desktop/AppStore_com.example.app.mobileprovision \
user@YOUR_MAC_IP:~/signing/
Move only the exact secret names you need — never dump a whole shell environment into a file, since it will contain unrelated API keys. Restore each secret value on the Mac side from your secret manager.
Create a short-lived build keychain so the private key does not linger in the login keychain between sessions. The security import + set-key-partition-list pattern below follows GitHub's documented macOS-runner recipe: Installing an Apple certificate on macOS runners for Xcode development.
#!/usr/bin/env bash
# sign-setup.sh — prepares a one-shot build keychain
set -euo pipefail
umask 077 # restrict perms before we touch secrets
KEYCHAIN_NAME="build-$(date +%Y%m%d%H%M%S).keychain"
KEYCHAIN_PATH="$HOME/Library/Keychains/${KEYCHAIN_NAME}-db"
KEYCHAIN_PASSWORD="$(openssl rand -base64 32)" # ephemeral; not stored
# Preserve the current user keychain search list so we can restore it
ORIGINAL_LIST=$(security list-keychains -d user | tr -d '"' | xargs)
cleanup() {
# Restore the original search list, then drop the ephemeral keychain
# shellcheck disable=SC2086
security list-keychains -d user -s $ORIGINAL_LIST || true
security delete-keychain "$KEYCHAIN_NAME" 2>/dev/null || true
}
trap cleanup EXIT
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
security set-keychain-settings -lut 21600 "$KEYCHAIN_NAME" # 6-hour timeout
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
# Prepend the build keychain; keep the user's original entries
# shellcheck disable=SC2086
security list-keychains -d user -s "$KEYCHAIN_NAME" $ORIGINAL_LIST
# P12_PASSWORD comes from your CI secret manager (never from shell history)
security import ~/signing/AppDist.p12 \
-P "$P12_PASSWORD" \
-A -t cert -f pkcs12 \
-k "$KEYCHAIN_NAME"
security set-key-partition-list \
-S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASSWORD" \
"$KEYCHAIN_NAME"
security find-identity -vp codesigning "$KEYCHAIN_NAME"
# If the next step is the build, disable the EXIT trap so the keychain stays.
# The build script must call its own cleanup (see end of this page).
trap - EXIT
echo "KEYCHAIN_NAME=$KEYCHAIN_NAME"
The -A flag on security import grants application access; set-key-partition-list then narrows it to codesign and security via Apple's partition identifiers. Skipping that call is the common cause of a codesign "allow access" dialog, which stalls any unattended build.
Xcode discovers profiles in one directory, keyed by UUID — the filename does not matter:
PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$PROFILE_DIR"
cp ~/signing/AppStore_com.example.app.mobileprovision "$PROFILE_DIR/"
# Sanity-check the embedded expiry date
security cms -D -i "$PROFILE_DIR/AppStore_com.example.app.mobileprovision" \
| plutil -extract ExpirationDate xml1 -o - - \
| grep string
xcodebuild archiveSet KEYCHAIN_NAME to the value printed by sign-setup.sh, then archive and export. Replace YourAppScheme and YourApp.xcworkspace with the real names from your project — this is an unexecuted example, not a benchmark.
set -euo pipefail
: "${KEYCHAIN_NAME:?run sign-setup.sh first and export KEYCHAIN_NAME}"
SCHEME="YourAppScheme"
WORKSPACE="YourApp.xcworkspace"
ARCHIVE_PATH="$HOME/build/YourApp.xcarchive"
EXPORT_PATH="$HOME/build/export"
EXPORT_OPTIONS="$HOME/signing/ExportOptions.plist"
xcodebuild archive \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration Release \
-archivePath "$ARCHIVE_PATH" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Apple Distribution" \
PROVISIONING_PROFILE_SPECIFIER="AppStore_com.example.app" \
OTHER_CODE_SIGN_FLAGS="--keychain $KEYCHAIN_NAME"
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportOptionsPlist "$EXPORT_OPTIONS" \
-exportPath "$EXPORT_PATH"
Accepted method values in ExportOptions.plist have changed across Xcode versions; run xcodebuild -exportArchive -help on the installed toolchain to confirm the exact string (recent Xcode lines accept app-store-connect; older ones use app-store).
<?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-connect</string>
<key>teamID</key> <string>YOUR_TEAM_ID</string>
<key>signingStyle</key> <string>manual</string>
</dict>
</plist>
Validate before uploading. If codesign --verify exits non-zero, do not treat the failed run as a successful build.
set -euo pipefail
IPA="$EXPORT_PATH/YourApp.ipa"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
unzip -q "$IPA" -d "$WORK"
APP_PATH=$(find "$WORK/Payload" -maxdepth 1 -name '*.app' -print -quit)
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
codesign -d --entitlements :- "$APP_PATH" | plutil -p -
| Error message | Likely cause |
|---|---|
a sealed resource is missing or invalid | A file changed after signing; re-archive from a clean state |
code object is not signed at all | A framework or dylib was not included in signing; check Embed & Sign |
resource envelope is obsolete | Legacy signature format; rebuild with the current toolchain |
| Entitlement mismatch | A capability in the app is not enabled on the provisioning profile; update the profile in the Apple Developer site |
On a reused Mac, drop the ephemeral keychain and the on-disk .p12 when the job is done — and restore the original keychain search list you captured earlier.
# shellcheck disable=SC2086
security list-keychains -d user -s $ORIGINAL_LIST
security delete-keychain "$KEYCHAIN_NAME"
rm -f ~/signing/AppDist.p12
Do not rely on if: always() to clean up after a machine failure or forced termination; verify cleanup before reusing the runner, or replace its workspace with a clean image and verify that no previous signing material remains.
Request a Mac sized for your Xcode version at Figment Mac hosting, or step through configuration at /mac-hosting/start. If Figment's managed CI setup (included in the $249 Managed Build Mac and $999 Managed Mac Team plans) fits better than running the above yourself, mention your signing targets in the quote.
Related guides: Swift Package Manager on a Remote Mac · SwiftUI Previews on a Rented Mac · GitHub Actions Self-Hosted Runner on Mac