The signing bottleneck nobody wants to fix
Code signing is the part of mobile CI everyone dreads. Your build pipeline is humming along, tests pass, artifacts are ready to ship—and then the signing step fails silently, or worse, prompts for a password that nobody remembers. We've all been there. In this guide, we'll walk through practical strategies that make code signing without pain a reality rather than a fantasy, covering the trade-offs so your team can pick the approach that fits your constraints.
Store keystores and certificates as CI secrets
The foundation of painless signing is treating keystores and certificates exactly as you'd treat database passwords: as encrypted secrets, never committed to version control.
For Android, your keystore file is the crown jewel. It's a binary blob that lives outside your repo, encrypted at rest in your CI system. Most CI platforms (GitHub Actions, GitLab CI, CircleCI, Bitrise) offer a secure secrets store. You upload the keystore once and reference it by name.
Here's a real scenario: your team maintains a CI/CD pipeline in GitHub Actions. You create a secret called ANDROID_KEYSTORE_BASE64 containing the base64-encoded keystore, plus two companion secrets: KEYSTORE_PASSWORD and KEY_ALIAS_PASSWORD.
In your workflow file, decode and stage the keystore:
- name: Decode keystore
run: |
echo ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | base64 -d > android/app/keystore.jks
- name: Build release APK
env:
KEYSTORE_PATH: android/app/keystore.jks
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: release_key
KEY_ALIAS_PASSWORD: ${{ secrets.KEY_ALIAS_PASSWORD }}
run: flutter build apk --releaseFor iOS, the process is similar but with certificates and provisioning profiles. You'll store the certificate (private key + public cert in .p12 format) and provisioning profile as base64 secrets. The trade-off: you're managing multiple files instead of one keystore, but each has a clear purpose.
The critical rule: never echo secrets to logs, never print them, never commit the decoded file back to the repo. Most CI systems automatically mask secrets in output, but verify this is enabled.
Use Fastlane match for iOS
If you're signing iOS apps manually—creating certificates in Apple's portal, downloading them, managing provisioning profiles—you're already in pain. Fastlane match eliminates most of this friction.
Match is a Fastlane tool that stores your signing certificates and provisioning profiles in a private Git repository (encrypted), then syncs them to your local machine or CI environment on demand. The workflow is: set it up once, then match handles certificate rotation, re-signing, and profile management automatically.
Here's how your team gets started:
- Create a private Git repository (GitHub, GitLab, etc.) to hold encrypted certificates.
- Install Fastlane:
sudo gem install fastlane - Run
fastlane match initin your Flutter project's iOS directory. - Answer prompts about storage type (Git is standard), repo URL, and Apple ID.
- Fastlane creates or imports your existing certificates, encrypts them, and pushes to the private repo.
In your CI pipeline, you then call match before signing:
- name: Setup iOS signing
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_PRIVATE_KEY: ${{ secrets.MATCH_GIT_PRIVATE_KEY }}
run: |
cd ios
fastlane match appstore --readonly
- name: Build iOS app
run: flutter build ios --releaseThe --readonly flag tells match not to create new certificates during CI—it only retrieves existing ones. This prevents accidental certificate creation in CI runs.
Match's strength is automation and visibility. Every certificate and profile is tracked in a Git repo with history. The trade-off is a new dependency (Fastlane, Ruby) and an extra Git repository to manage. For teams shipping iOS regularly, this is a net win. For teams building iOS once a year, the overhead might not be justified.
Document the process
Your code signing setup will be forgotten. Someone will leave the team, a laptop will die, CI secrets will accidentally get rotated. Without documentation, the next person to touch this system will spend hours reverse-engineering your choices.
Create a living document—a markdown file in your repo, or a Confluence page—that covers:
- Where secrets are stored: "iOS certificates are in the private
certificatesrepo accessed via Fastlane match. Android keystore is encrypted in GitHub Actions secrets." - How to rotate secrets: "To renew the Android keystore, decode the old one locally, generate a new one with the same alias, re-encode as base64, and update the GitHub Actions secret."
- Emergency procedures: "If CI signing fails, check that the keystore password in secrets matches the actual keystore. If provisioning profiles are expired, run
fastlane match nuke distributionto regenerate." - Local setup for new team members: "Clone the certificates repo. Run
fastlane match developmentto download certs locally. Configure your Xcode team ID." - Audit trail: "Certificate expiration dates, last rotation dates, and who has access to which secrets."
A five-minute read now saves a two-hour firefighting session later.
Back up signing keys securely
Your keystore and certificates are irreplaceable. If they're lost, you can't sign updates to your app in Google Play or TestFlight. A drive failure, a CI secret accidentally deleted, a repository wiped—any of these becomes a crisis without backups.
The backup strategy depends on your scale:
Small team or side project: Encrypted backup on your personal machine plus cloud storage. Download the keystore locally, encrypt it with GPG, store the encrypted file in a personal AWS S3 bucket or encrypted USB drive locked in a drawer.
Medium to large team: Use your CI system's native backup or a dedicated secrets management tool. Bitrise, for example, has built-in secret versioning and rollback. AWS Secrets Manager or HashiCorp Vault offer audit logs showing who accessed what and when.
Enterprise: Integrate with your organization's secrets management infrastructure. Many enterprises use HashiCorp Vault or AWS Secrets Manager centrally, with CI pipelines pulling secrets via API. This gives you compliance, audit trails, and a single source of truth.
A practical example for mid-sized teams using GitHub Actions:
- name: Back up Android keystore
if: github.event_name == 'schedule' # Run nightly
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
echo ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | base64 -d > keystore.jks
aws s3 cp keystore.jks s3://backups-encrypted/keystores/$(date +%Y-%m-%d)-backup.jks
rm keystore.jksThis scheduled job backs up the keystore to S3 nightly. Version the bucket, enable MFA delete protection, and restrict IAM access to this bucket tightly. The encrypted keystore in GitHub Actions secrets is the primary; S3 is the fallback.
Common mistakes
- Committing keystores to version control: Even in a "private" repo. Use
.gitignoreto explicitly exclude*.jksand*.p12files. Assume any committed secret will be found. - Using the same keystore password and key alias password: If one is leaked, both are compromised. Use different, strong passwords for each.
- Not rotating certificates before expiry: Set calendar reminders for 60 days before expiry. Expired certificates break builds with cryptic errors. Renewal takes minutes if you're prepared, hours if you're not.
- Storing match encryption password in CI secrets without rotation: If the password is leaked, an attacker can clone the certificates repo and decrypt everything. Rotate this password at least annually.
- Forgetting to limit CI secret scope: GitHub Actions allows per-branch secret visibility. Enable this for production secrets so they're only available during release builds, not on every pull request.
- Letting a single person own the signing setup: If one developer holds the only backup or knows the only password, you have a bus factor problem. At least two people should be able to recover signing credentials.
Key takeaways
- Store keystores and certificates exclusively in your CI system's encrypted secrets store, never in version control.
- For iOS, use Fastlane match to automate certificate management and store the encrypted certificates in a private Git repository.
- Document your signing process, rotation procedures, and emergency recovery steps so the next person (or you, in six months) can act quickly.
- Back up your keystores and certificates independently; losing signing credentials locks you out of app stores.
- Rotate signing credentials annually and before expiry; set explicit reminders and verify expiration dates in CI logs.
- Distribute knowledge and access across the team so signing credentials aren't a single point of failure.






Comments
Loading comments…