Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Manage Secrets and Env Vars in iOS CI Pipelines

Hardcoded credentials in a CI script are one commit away from a public git history. Five values show up in almost every iOS pipeline: team ID, certificate password, App Store Connect API key, provisioning profile UUID, and App Store Connect issuer ID. This page moves all five into CI secrets, injects them correctly, and verifies none appear in logs or artefacts.

Where iOS CI Secrets Leak From

Audit the current pipeline for these common exposure points before you change anything:

  • xcodebuild ... DEVELOPMENT_TEAM=XXXXXXXXXX in a shell script committed to the repo.
  • security import cert.p12 -P "hardcoded_password" with the password on the command line.
  • fastlane deliver pointing to an API key file inside the repo.
  • Leftover echo "$MATCH_PASSWORD" from a debug session.
  • A diagnostics step that runs env or printenv into an artefact.

Never dump the whole environment to a file. Transfer the required variable names to the new system separately and populate the values from your secret manager at runtime; a plaintext env snapshot captures every secret the shell can see.

GitHub Actions Secrets: Levels, Size and Forks

GitHub Actions supports repository, environment, and organisation secrets, and documents a 48 KB maximum per secret value, with a workaround pattern for larger payloads (GitHub Actions: using secrets). Binary signing material such as .p12 certificates usually fits once base64-encoded; if it does not, split or store the decryption passphrase as the secret and keep the encrypted blob in the repo.

Two behaviours matter for pipeline design:

  • Secrets other than GITHUB_TOKEN are not passed to runners triggered from forked repositories. If you accept external PRs, do not rely on signing succeeding on fork-triggered workflows.
  • GitHub redacts registered secret values from job output. Values that are derived from a secret (for example, the decoded binary) are not automatically masked; use ::add-mask::VALUE to register derived sensitive strings.

Secrets Inventory for the Five Values

SecretStorage typeFormatInjectionLog check
APPLE_TEAM_IDEnv secret10-char stringenv: on the build stepgrep log for the 10-char value
CERT_P12_PASSWORDEnv secretStringenv: then -P "$CERT_P12_PASSWORD"grep log for the password
APP_STORE_CONNECT_API_KEY_BASE64Env secretBase64 of .p8Decode to a temp file at runtimegrep log for -----BEGIN
APP_STORE_CONNECT_ISSUER_IDEnv secretUUIDenv: passed to Fastlanegrep log for the UUID
PROVISIONING_PROFILE_UUIDEnv secretUUIDenv: referenced in Fastfilegrep log for the UUID
CERT_P12_BASE64Env secretBase64 of .p12Decode to temp file; security importconfirm no -----BEGIN CERTIFICATE in log

Keep the base64 form as the registered secret. GitHub will then redact the base64 string from logs even if a step echoes it by mistake.

Base64 Encode, Decode Safely, Clean Up

Encode once locally; store the output as a secret:

base64 -i AuthKey_XXXXXXXXXX.p8 | tr -d '\n'
base64 -i Certificates.p12      | tr -d '\n'

Decode in the job with a restrictive umask so the files are never world-readable, and clean up on failure:

- name: Decode signing assets
  env:
    ASC_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
    CERT_P12_BASE64:    ${{ secrets.CERT_P12_BASE64 }}
  run: |
    set -euo pipefail
    umask 077
    WORKDIR="$(mktemp -d)"
    echo "WORKDIR=$WORKDIR" >> "$GITHUB_ENV"
    printf '%s' "$ASC_API_KEY_BASE64" | base64 --decode > "$WORKDIR/asc_api_key.p8"
    printf '%s' "$CERT_P12_BASE64"    | base64 --decode > "$WORKDIR/Certificates.p12"

- name: Import certificate
  env:
    CERT_P12_PASSWORD: ${{ secrets.CERT_P12_PASSWORD }}
  run: |
    set -euo pipefail
    ORIG_LIST=$(security list-keychains -d user | tr -d '"')
    security create-keychain -p "" build.keychain
    security unlock-keychain -p "" build.keychain
    security set-keychain-settings -lut 3600 build.keychain
    security import "$WORKDIR/Certificates.p12" \
      -k build.keychain \
      -P "$CERT_P12_PASSWORD" \
      -T /usr/bin/codesign
    security list-keychains -d user -s build.keychain $ORIG_LIST

- name: Clean up
  if: always()
  run: rm -rf "${WORKDIR:-}"

Two details worth keeping straight. security list-keychains -s replaces the search list; the snippet above preserves the previous entries and appends build.keychain. if: always() runs on normal step failure; it cannot guarantee cleanup after a runner machine is killed, so never store long-lived secrets on the runner disk.

Provisioning profiles are not imported into the Keychain. Write the .mobileprovision file to ~/Library/MobileDevice/Provisioning Profiles/ under its UUID and let xcodebuild or Fastlane match pick it up.

Injecting Secrets into Fastlane and xcodebuild

Fastlane reads most inputs from environment variables, but the exact name depends on the action. Run fastlane action <name> to list parameters and their env var equivalents for your installed version (Fastlane key management):

fastlane action app_store_connect_api_key   # key id, issuer id, key filepath
fastlane action match                       # MATCH_PASSWORD, MATCH_GIT_URL, ...
fastlane action pilot                       # TestFlight upload inputs

Pass secrets to xcodebuild as build settings, never on the shell command line as literals:

- name: Archive
  env:
    APPLE_TEAM_ID:     ${{ secrets.APPLE_TEAM_ID }}
    MATCH_PASSWORD:    ${{ secrets.MATCH_PASSWORD }}
  run: |
    xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" \
      -configuration Release -destination 'generic/platform=iOS' \
      DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
      -archivePath "$WORKDIR/App.xcarchive" archive

Replace $WORKSPACE and $SCHEME with the real values from your project (xcodebuild -list prints them); do not guess a scheme name. Do not commit a .env file with real secrets, even with .gitignore: a force-push or mis-scoped ignore pattern is the usual reason production keys end up on GitHub.

Verify Masking and Audit Logs

After the first green run, spot-check masking with a disposable step and then delete it:

- name: One-shot masking check (delete after use)
  env:
    APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
  run: echo "team=$APPLE_TEAM_ID"   # expect: team=***

Download the raw log and grep for the real values from a trusted machine:

grep -F "$KNOWN_TEAM_ID"           full-build.log
grep -E "BEGIN (PRIVATE KEY|CERTIFICATE|EC PRIVATE KEY)" full-build.log

Also scan .xcresult bundles, test reports, and .ipa artefacts. A debugging env/printenv step is the single most common source of accidental secret exposure in CI.

Rotate Secrets Without Breaking In-Flight Builds

  1. Add the new value under a parallel name (MATCH_PASSWORD_V2).
  2. Point the workflow at the new name on a feature branch; confirm a green build.
  3. Merge to default.
  4. Delete the old secret once no in-flight runs reference it.

Updating a secret value in place while jobs are running can race with step startup and surface as an empty string in some platforms.

A Mac Runner That Fits This Pipeline

Every step above runs on macOS: security, codesign, xcodebuild, Fastlane's signing actions. The right Figment plan depends on which Xcode you need.

  • The $29 virtual Intel macOS environment runs macOS and the Keychain/security/codesign toolchain. It is suitable for pipelines pinned to an Intel-compatible Xcode (Xcode 26 or earlier per Apple's porting guidance). Specs: 2 vCPU, 8 GiB RAM, 80 GiB disk; browser-console access; no GPU acceleration or audio.
  • The i3 at $49/month is a physical Intel option on the same Xcode-26-or-earlier basis; collected by request until supply is confirmed.
  • M-series from $95/month is the path for ARM-native builds, GPU-assisted simulators, and projects that must track Xcode 27. Chip, RAM and disk are confirmed in the quote.

Worked cost for a solo iOS CI on this stack: Intel VM at $29 × 12 = $348/year plus a free GitHub Actions self-hosted runner install. Compare to CloudMac's published $999/year M4/16 GB/256 GB offer (cloudmac.org/pricing) when you need Apple silicon: that is $83.25 effective per month and $141 below 12 × Figment's $95 entry, but on hardware Figment confirms per quote rather than a fixed M4 row. Choose the $29 VM for Intel-Xcode signing work, and Figment M-series when ARM execution or the latest Xcode is a hard requirement. Tax and any fees apply as stated in each provider's actual offer.

Request a configuration at /mac-hosting or start here.


Related guides:


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms