Security & Credentials
How ShipItSwifty stores, uses, and protects your signing assets and store credentials.
On this page
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
scopeclaim 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
| Secret | Storage |
|---|---|
ASC private key (.p8) | Env var ASC_PRIVATE_KEY in CI; a local file path outside version control on dev machines |
| Vault passphrase | Env var VAULT_PASSWORD — encrypts/decrypts the certificate repository |
| Google Play service account JSON | Env var GOOGLE_PLAY_SERVICE_ACCOUNT_JSON in CI; a local file path on dev machines |
| Webhook URLs (Slack, Teams) | Env vars |
| Keychain password | Auto-generated per CI run, held in memory only |
Threat model
| Threat | Mitigation |
|---|---|
.p8 key leakage | CI secrets vault; local .p8 paths kept outside version control; key material is never committed |
| JWT theft | 15-minute expiry, scoped tokens, HTTPS-only |
| Certificate repo exposure | AES-256-GCM encryption; scrypt key derivation with per-file salt resists offline brute force of the passphrase |
.p8 exposure to other local users | Staged key written with 0600 permissions inside a 0700 directory |
| CI log exposure | Automatic 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 cleanupApp 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.
| Value | Where to find it |
|---|---|
team_id | Usually auto-detected from Xcode signing. Otherwise: the 10-character Apple Developer Team ID shown in Certificates, IDs & Profiles |
ASC_KEY_ID | App Store Connect → Users and Access → Integrations → App Store Connect API → the key's Key ID |
ASC_ISSUER_ID | Same page as the Key ID. This is not stored inside the downloaded .p8 file |
ASC_PRIVATE_KEY_PATH | Local filesystem path to the downloaded .p8 file |
ASC_PRIVATE_KEY | Raw contents of the .p8 file — use this form in CI secrets |
Note:
team_idandASC_ISSUER_IDare different values. Ifshipit generateor Xcode already resolves the correct team, you usually don't need to setteam_idat 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.p8CI (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 contentsGoogle Play credentials (Android)
shipit play-store authenticates with a Google Cloud service account that has Play Console permissions.
| Value | Where to find it |
|---|---|
package_name | Your Android application ID — applicationId in app/build.gradle(.kts) |
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON | Raw contents of the downloaded service-account JSON key |
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH | Local filesystem path to that JSON key file |
Setup flow:
- Open the Google Cloud Console and create or select a project.
- Enable the Google Play Developer API.
- Create a service account and download a JSON key for it.
- In Play Console → Users and permissions, invite the service account's email address.
- 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
- 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.
- Verify the API is enabled in the same Cloud project that owns the service account.
- Confirm the correct key — the
client_emailin your JSON key must match the email invited in Play Console. - 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 doctorRelated pages
- CI Setup — wiring these secrets into GitHub Actions, Bitrise, and self-hosted runners
- Configuration Reference — every
Shipfile.ymlkey - iOS Quickstart and Android Quickstart