Skip to content
ExponaBLOG
FLUTTER · DAY 081 / 100

Code signing without pain: practical tips and trade-offs

The part of mobile CI everyone dreads.

Expona Admin6 min read

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:

dart
- 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 --release

For 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:

  1. Create a private Git repository (GitHub, GitLab, etc.) to hold encrypted certificates.
  2. Install Fastlane: sudo gem install fastlane
  3. Run fastlane match init in your Flutter project's iOS directory.
  4. Answer prompts about storage type (Git is standard), repo URL, and Apple ID.
  5. 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:

dart
- 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 --release

The --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 certificates repo 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 distribution to regenerate."
  • Local setup for new team members: "Clone the certificates repo. Run fastlane match development to 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:

dart
- 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.jks

This 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 .gitignore to explicitly exclude *.jks and *.p12 files. 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.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles