Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Sign iOS Builds with App Store Connect API Key

Replace Apple ID auth with an App Store Connect API key, keep the .p8 out of Git and out of env > file dumps, and run the pipeline on a Mac that matches your toolchain. For device builds and Apple-silicon targets a Figment M-series Mac from $95/month is the right fit (exact chip, RAM and disk confirmed in the quote). A Figment $29 virtual Intel macOS instance can drive the signing and upload steps when you are targeting a compatible Intel Xcode (Xcode 26 or earlier); it is a real macOS environment.

Why Apple ID Auth Fails in Headless CI

Apple ID authentication in older Fastlane and Xcode flows relies on a session cookie that typically lapses within about 30 days. When it expires, the next TestFlight upload, cert fetch, or App Store Connect query fails with a 2FA prompt that no headless runner can answer, and the release pipeline stalls until someone re-authenticates on a trusted device.

The App Store Connect API uses a private key to sign short-lived JWTs instead. There is no interactive session, no 2FA, and the key is valid until you revoke it. Fastlane's App Store Connect API documentation describes the same action-based flow for match, gym, pilot, deliver and precheck.

Creating the API Key with the Right Role

  1. Sign in to App Store Connect, open Users and Access, pick the Integrations tab, then App Store Connect API.
  2. Create a Team Key rather than an individual key so provisioning and signing endpoints are reachable.
  3. Assign the narrowest role that works. App Manager is enough for signing, certificate and TestFlight operations; reserve Admin for keys that must manage users.
  4. Download the .p8 file immediately — App Store Connect shows it once and does not expose a re-download.
  5. Record the Key ID (10-character alphanumeric) and the Issuer ID (UUID at the top of the page).

Store the three values as CI secrets in your provider's secret manager. Do not dump the entire environment to a file; env > file leaks every unrelated secret at the same time. If you need to carry secrets to a new CI, transfer the names and let each system re-inject the values.

Loading the .p8 Safely from a CI Secret

Base64-encode the .p8 once so string-only secret stores (GitHub Actions, GitLab CI, Bitrise, Jenkins Credentials) can hold it verbatim:

# Run locally, paste the output into the CI secret value
base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy

Then add three secrets: ASC_KEY_CONTENT (the base64 text), ASC_KEY_ID, and ASC_ISSUER_ID.

The script below is the artifact for this guide. Source it at the start of the job. It tightens the umask before touching disk, writes the key to a private temp path, and removes it on exit — success or failure — with a trap.

#!/usr/bin/env bash
# setup-asc-key.sh — source me: `source ./setup-asc-key.sh`
# Requires CI secrets: ASC_KEY_CONTENT (base64 .p8), ASC_KEY_ID, ASC_ISSUER_ID
set -euo pipefail

: "${ASC_KEY_CONTENT:?Set ASC_KEY_CONTENT to the base64-encoded .p8 content}"
: "${ASC_KEY_ID:?Set ASC_KEY_ID to the 10-character key ID}"
: "${ASC_ISSUER_ID:?Set ASC_ISSUER_ID to the team issuer UUID}"

# Restrictive perms BEFORE any file is created
_old_umask=$(umask)
umask 077

_ASC_KEY_DIR=$(mktemp -d "${TMPDIR:-/tmp}/asc-key.XXXXXX")
_ASC_KEY_FILE="$_ASC_KEY_DIR/AuthKey_${ASC_KEY_ID}.p8"

cleanup() { rm -rf "$_ASC_KEY_DIR"; }
trap cleanup EXIT INT TERM

printf '%s' "$ASC_KEY_CONTENT" | base64 --decode > "$_ASC_KEY_FILE"
umask "$_old_umask"

export ASC_KEY_PATH="$_ASC_KEY_FILE"
# Read by Fastlane's app_store_connect_api_key action when invoked with no args
export APP_STORE_CONNECT_API_KEY_KEY_ID="$ASC_KEY_ID"
export APP_STORE_CONNECT_API_KEY_ISSUER_ID="$ASC_ISSUER_ID"
export APP_STORE_CONNECT_API_KEY_KEY_FILEPATH="$_ASC_KEY_FILE"

echo "ASC key staged at $_ASC_KEY_FILE (removed when this shell exits)"

Note that a separate CI step runs in a separate shell: the trap only cleans up inside the shell that sourced the script. If your CI splits steps across shells, add an explicit teardown step and treat machine-failure cleanup as the runner's responsibility, not the script's.

The Fastfile side is a single call that every other action can reuse:

# Fastfile
lane :build_and_upload do
  api_key = app_store_connect_api_key(
    key_id:       ENV.fetch("ASC_KEY_ID"),
    issuer_id:    ENV.fetch("ASC_ISSUER_ID"),
    key_filepath: ENV.fetch("ASC_KEY_PATH"),
    duration:     1200,   # seconds; Fastlane caps this at 1200
    in_house:     false
  )

  match(type: "appstore", api_key: api_key, readonly: true)
  gym(scheme: ENV.fetch("APP_SCHEME"), configuration: "Release")
  pilot(api_key: api_key, skip_waiting_for_build_processing: true)
end

Pass the project's real scheme through APP_SCHEME — do not hard-code a placeholder your CI has no way to build.

Using the Key with xcodebuild Directly

For projects without Fastlane, three flags cover archive and -exportArchive:

xcodebuild \
  -scheme "$APP_SCHEME" \
  -configuration Release \
  -archivePath "build/${APP_SCHEME}.xcarchive" \
  -authenticationKeyPath     "$ASC_KEY_PATH" \
  -authenticationKeyID       "$ASC_KEY_ID" \
  -authenticationKeyIssuerID "$ASC_ISSUER_ID" \
  archive

Use the same three flags for the -exportArchive step that uploads to App Store Connect. Keep set -e on the job so a failed signing step stops the pipeline; a build that exits non-zero is not a benchmark.

Fail Fast: Verify the Key Before You Archive

A 20-minute archive followed by an auth failure wastes a runner slot. Run a tiny verification lane first so bad keys surface in seconds:

lane :verify_asc_key do
  api_key = app_store_connect_api_key(
    key_id:       ENV.fetch("ASC_KEY_ID"),
    issuer_id:    ENV.fetch("ASC_ISSUER_ID"),
    key_filepath: ENV.fetch("ASC_KEY_PATH"),
    duration:     600
  )
  Spaceship::ConnectAPI.token = api_key
  apps = Spaceship::ConnectAPI::App.all(limit: 1)
  UI.user_error!("No apps visible to this key") if apps.empty?
  UI.success("ASC key valid — #{apps.length} app visible")
end

Wire fastlane verify_asc_key as a short-timeout job ahead of the archive job in your pipeline graph. Only the full archive depends on it, so a bad rotation fails in under a minute instead of after twenty.

Rotating the Key Without Downtime

Keys cannot be edited in place, but two keys can be valid at once:

  1. Generate a new team key in App Store Connect; leave the current one active.
  2. Update CI secrets to the new Key ID, Issuer ID, and .p8.
  3. Run verify_asc_key and one full release-branch build against the new key.
  4. Revoke the old key once the new one has shipped a build.

Name keys with a clear purpose and date (CI-prod-2026-11) so the revoke decision is obvious. Document who holds the downloaded .p8 for break-glass access — remember that App Store Connect does not re-issue it.

Picking the Right Figment Mac for This Job

Match the host to the toolchain, not the other way round:

ScenarioFigment planMonthly priceNotes
App uses Xcode 26 or earlier, Intel-compatible targets, signing + upload onlyVirtual Intel macOS (2 vCPU, 8 GiB RAM, 80 GiB disk)$29Real macOS on Linux/KVM; browser-console access; no GPU/audio. Intel Xcode runs here.
iOS/macOS device builds, Apple-silicon simulators, Xcode 27M-series Macfrom $95Entry price; exact chip, RAM and disk confirmed in the quote.
Signing + CI runner managed for youManaged Build Mac$249One M-series Mac, CI setup, backups, monitoring, 30 minutes monthly support.
Two parallel runners with team onboardingManaged Mac Team$999Two M-series Macs, two CI runners, team setup, backups, monitoring, one hour monthly support.

A worked example: your verify_asc_key + archive + TestFlight upload pipeline that currently runs on a laptop-under-a-desk. Renting an M-series Mac at $95/month replaces the laptop at $95 × 12 = $1,140 per year (tax and fees as stated in the actual offer). CloudMac publishes an annual rate of $999/year for an M4 / 16 GB / 256 GB Mac mini (equivalent to $83.25/month effective), and $99.99/month on its pricing page — cheaper on annual than the Figment entry tier, on a specific published spec. Figment's $95 is an entry price for an M-series machine with the exact spec confirmed on quote, and the Managed Build and Managed Mac Team plans bundle the CI setup, backups, monitoring and support time that a bare Mac rental does not. If what you need is the Mac only, price both; if what you need is the signing pipeline running next week without you building it, the managed tiers are the comparison to run.

Physical i3 Mac ($49) and physical M-series requests collect in a queue until supply is confirmed — ask for a current slot rather than assuming stock. The $29 virtual Intel macOS tier is the one to pick when the job is "sign and upload with an older Intel-compatible Xcode and nothing else."

Ready to run this pipeline on a Figment Mac? Start here →

Related guides:


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms