Skip to content
Guides

Security & Credentials

How ShipItSwifty stores, uses, and protects your signing assets and store credentials.

ShipItSwifty handles the most sensitive material in your release pipeline — App Store Connect keys, signing certificates, and Google Play service accounts. This page explains exactly where each secret lives, how it is protected, and how to wire credentials up safely. Nothing here requires trust in a third party: the CLI runs entirely on your machine and collects zero telemetry.

Security principles

  • Never log secrets — the logger automatically redacts any value from environment variables matching *KEY*, *SECRET*, *TOKEN*, or *PASSWORD*, so secrets cannot leak into CI logs.
  • Temporary keychains — CI runs create an ephemeral keychain scoped to the process and destroy it afterwards (shipit sign sync --ci / shipit sign cleanup).
  • Minimal token scope — App Store Connect JWTs use a scope claim that limits the API surface per operation.
  • Short token lifetime — JWTs expire after 15 minutes by default and auto-refresh.
  • Encrypted at rest — the certificate vault uses AES-256-GCM with a key derived from your team passphrase via scrypt (N=2¹⁷, r=8, p=1) and a random per-file salt.
  • No telemetry — ShipItSwifty collects zero usage data and never phones home.

Where each secret lives

SecretStorage
ASC private key (.p8)Env var ASC_PRIVATE_KEY in CI; a local file path outside version control on dev machines
Vault passphraseEnv var VAULT_PASSWORD — encrypts/decrypts the certificate repository
Google Play service account JSONEnv var GOOGLE_PLAY_SERVICE_ACCOUNT_JSON in CI; a local file path on dev machines
Webhook URLs (Slack, Teams)Env vars
Keychain passwordAuto-generated per CI run, held in memory only

Threat model

ThreatMitigation
.p8 key leakageCI secrets vault; local .p8 paths kept outside version control; key material is never committed
JWT theft15-minute expiry, scoped tokens, HTTPS-only
Certificate repo exposureAES-256-GCM encryption; scrypt key derivation with per-file salt resists offline brute force of the passphrase
.p8 exposure to other local usersStaged key written with 0600 permissions inside a 0700 directory
CI log exposureAutomatic secret redaction in all log output

The encrypted certificate vault

Certificates and provisioning profiles are stored encrypted in a shared Git repository, so your whole team signs with the same assets. The AES-256 key is derived from the VAULT_PASSWORD passphrase — only teammates who know the passphrase can decrypt anything.

# One-time setup: create the encrypted cert repository
shipit sign init
 
# On each machine / CI run: fetch and install signing assets
shipit sign sync --ci
 
# After a CI run: remove the temporary keychain
shipit sign cleanup

App Store Connect credentials (iOS)

Only Apple-backed commands need these: upload, testflight, metadata, and provision. Local commands like build, test, archive, and export work without them.

ValueWhere to find it
team_idUsually auto-detected from Xcode signing. Otherwise: the 10-character Apple Developer Team ID shown in Certificates, IDs & Profiles
ASC_KEY_IDApp Store Connect → Users and Access → Integrations → App Store Connect API → the key's Key ID
ASC_ISSUER_IDSame page as the Key ID. This is not stored inside the downloaded .p8 file
ASC_PRIVATE_KEY_PATHLocal filesystem path to the downloaded .p8 file
ASC_PRIVATE_KEYRaw contents of the .p8 file — use this form in CI secrets

Note: team_id and ASC_ISSUER_ID are different values. If shipit generate or Xcode already resolves the correct team, you usually don't need to set team_id at all.

Local development:

export ASC_KEY_ID=XXXXXXXXXX
export ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export ASC_PRIVATE_KEY_PATH=./.secrets/AuthKey_XXXXXXXXXX.p8

CI (store these as encrypted secrets, never in the repo):

env:
  ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
  ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
  ASC_PRIVATE_KEY: ${{ secrets.ASC_PRIVATE_KEY }} # raw .p8 contents

Google Play credentials (Android)

shipit play-store authenticates with a Google Cloud service account that has Play Console permissions.

ValueWhere to find it
package_nameYour Android application ID — applicationId in app/build.gradle(.kts)
GOOGLE_PLAY_SERVICE_ACCOUNT_JSONRaw contents of the downloaded service-account JSON key
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATHLocal filesystem path to that JSON key file

Setup flow:

  1. Open the Google Cloud Console and create or select a project.
  2. Enable the Google Play Developer API.
  3. Create a service account and download a JSON key for it.
  4. In Play Console → Users and permissions, invite the service account's email address.
  5. Under App permissions, add your app and grant release permissions for the tracks you use (internal, alpha, beta, or production).

Important: Only the Play Console account owner can invite users — admin access is not enough.

Troubleshooting 403 PERMISSION_DENIED

  1. Check app-level permissions — the service account needs permissions for your specific app under the App permissions tab; account-level permissions alone are not enough.
  2. Verify the API is enabled in the same Cloud project that owns the service account.
  3. Confirm the correct key — the client_email in your JSON key must match the email invited in Play Console.
  4. Wait for propagation — permission changes can take a few minutes.

Verifying your setup

# Print every resolved credential source (values are redacted)
shipit env
 
# Check for expired certificates, missing keys, and common misconfigurations
shipit doctor