Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Set Up Xcode Code Signing on a Remote Mac

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.

What Code Signing Actually Requires

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.

  1. A signing certificate (Distribution or Development) stored in the Mac's Keychain, including its private key. The certificate record lives in your Apple Developer account; the private key was generated on the Mac where the Certificate Signing Request was created and is not held by Apple. Export both together as an encrypted .p12.
  2. A provisioning profile (.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.
  3. An unlocked Keychain at build time so 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.

Exporting a p12 From Your Local Mac

On the Mac where the CSR was originally created:

  1. Open Keychain Access (/Applications/Utilities/Keychain Access.app).
  2. Under My Certificates, find your certificate — it will have a disclosure triangle showing the private key beneath it. Select the certificate and its private key together.
  3. Right-click → Export 2 items and choose the .p12 format.
  4. Set a strong passphrase. Store the passphrase in your team secret manager (GitHub Actions secrets, Vault, 1Password), not a plaintext file.

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.

Importing the p12 Into a Build Keychain

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.

Installing the Provisioning Profile

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

Running xcodebuild archive

Set 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>

Verifying the Archive Signature

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 messageLikely cause
a sealed resource is missing or invalidA file changed after signing; re-archive from a clean state
code object is not signed at allA framework or dylib was not included in signing; check Embed & Sign
resource envelope is obsoleteLegacy signature format; rebuild with the current toolchain
Entitlement mismatchA capability in the app is not enabled on the provisioning profile; update the profile in the Apple Developer site

Cleaning Up After the Build

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


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms