Tutorial: configure Fastlane Match with GitHub Actions to publish an iOS app without storing certificates in the repo
Apps mobile · By Kevin Marcondes · Published on · 9 min read
This tutorial shows how to configure Fastlane Match with GitHub Actions to publish an iOS app without storing certificates in the code repository. You will create an encrypted certificate repo, secure GitHub secrets, use a temporary keychain on the macOS runner, and automate build and TestFlight upload with a dedicated Fastlane lane.
Prerequisites and secure architecture (Fastlane Match + CI)
The architecture pairs Fastlane Match to manage certificates and provisioning profiles with a GitHub Actions workflow running on macOS. Certificates live in a private, encrypted Git repository, never in the app source repo. The CI uses a temporary keychain on the runner and protected secrets.
You will need:
- An Apple Developer Program account with permissions to create certificates and profiles.
- macOS with Xcode and command line tools.
- Ruby and Bundler installed to run Fastlane.
- A separate private Git repository for certificates (Match).
- A GitHub repository with Actions enabled and a macOS runner.
- Permission in App Store Connect to create an API key.
To reinforce secrets handling best practices, see the pipeline checklist on CI/CD runners & secrets.
Step 1 — Initialize Fastlane in the iOS project
Fastlane organizes build, signing and distribution tasks into lanes. Let's prepare the project with Bundler to lock the Fastlane version and avoid differences between machines. Run these steps on a macOS machine with Xcode installed.
# Dentro do diretório do app iOS
bundle init
# Adicione o Fastlane ao Gemfile
echo 'gem "fastlane"' >> Gemfile
bundle install
# Inicialize o Fastlane (responda os prompts conforme seu projeto)
bundle exec fastlane init
Step 2 — Configure the Matchfile and the certificate repository
Fastlane Match stores certificates and profiles in a private Git repo encrypted with a password. Do not use the app's code repository — keep a dedicated repository just for signing artifacts. Below is an example Matchfile for App Store distribution.
- Keep the certificate repository private with minimal access.
- Use a dedicated branch (for example, "certs").
- In CI, run Match in readonly mode.
# Matchfile (na pasta fastlane do projeto)
# Ajuste os valores conforme seu app/time
git_url("[email protected]:org/ios-certs.git")
storage_mode("git")
git_branch("certs")
type("appstore")
app_identifier(["com.example.app"]) # seu bundle id
username("[email protected]") # Apple ID
# Keychain temporário controlado por variáveis de ambiente no CI
keychain_name(ENV.fetch("KEYCHAIN_NAME", "ci"))
keychain_password(ENV.fetch("KEYCHAIN_PASSWORD", "temp-pass"))
Step 3 — Create certificates and profiles locally using Match password
Generate certificates and profiles once in a trusted local environment. This populates the certificate repository with encrypted artifacts. Never commit certificates or provisioning profiles to the app repository.
- Choose clear branch names and keep a history.
- Store the Match password in a secrets vault before configuring CI.
- Running development/adhoc types also helps internal testing.
# Defina a senha que criptografa os artefatos do Match
export MATCH_PASSWORD='sua-senha-forte'
# Gere certificados e perfis para App Store (primeira vez)
bundle exec fastlane match appstore
# Opcional: development para testes locais
bundle exec fastlane match development
# CUIDADO: o comando abaixo revoga tudo do tipo especificado
# **Use somente se precisar girar chaves/perfis comprometidos.**
# bundle exec fastlane match nuke appstore
How to protect secrets in GitHub Actions?
Secrets should live in GitHub Actions Secrets/Environments, never in versioned files. The App Store Connect .p8 private key must not be stored in the repository. Use protected environments and manual approvals for sensitive jobs.
See deployment pipeline best practices at the automatic deployment pipeline.
- Restrict who can read/write secrets and who can trigger releases.
- Use Environments with protections (reviewers) for production.
- Rotate the Match password and API key periodically.
| Secret/variable | Purpose |
|---|---|
| MATCH_PASSWORD | Password that encrypts Match's certificate repository |
| MATCH_GIT_URL | URL of the private certificates repository |
| MATCH_GIT_BRANCH | Certificates branch (e.g. certs) |
| APP_STORE_CONNECT_API_KEY_ID | Key ID for the App Store Connect API key |
| APP_STORE_CONNECT_ISSUER_ID | Issuer ID for the organization in App Store Connect |
| APP_STORE_CONNECT_PRIVATE_KEY | Content of the .p8 private key (text) |
| KEYCHAIN_PASSWORD | Password for the temporary keychain on the runner |
Step 4 — Prepare the Fastfile with a CI lane
Create a lane that sets up the temporary keychain, consumes the App Store Connect API key from environment variables, and calls Match in readonly mode. Do not hardcode sensitive values in the Fastfile. The lane below builds and uploads to TestFlight.
- Set the correct app scheme.
- Adjust export_method and other build options according to your signing setup.
- Delete the keychain at the end of the job to reduce attack surface.
# Fastfile (exemplo simplificado)
default_platform(:ios)
platform :ios do
lane :ci_appstore do
api_key = app_store_connect_api_key(
key_id: ENV['APP_STORE_CONNECT_API_KEY_ID'],
issuer_id: ENV['APP_STORE_CONNECT_ISSUER_ID'],
key_content: ENV['APP_STORE_CONNECT_PRIVATE_KEY'],
in_house: false
)
create_keychain(
name: ENV.fetch('KEYCHAIN_NAME', 'ci'),
password: ENV.fetch('KEYCHAIN_PASSWORD', 'temp-pass'),
default_keychain: true,
unlock: true,
timeout: 3600,
lock_when_sleeps: true
)
match(
type: 'appstore',
readonly: true,
keychain_name: ENV.fetch('KEYCHAIN_NAME', 'ci'),
keychain_password: ENV.fetch('KEYCHAIN_PASSWORD', 'temp-pass'),
git_url: ENV['MATCH_GIT_URL'],
git_branch: ENV.fetch('MATCH_GIT_BRANCH', 'certs')
)
build_ios_app(
scheme: 'YourAppScheme',
export_method: 'app-store'
)
upload_to_testflight(api_key: api_key)
delete_keychain(name: ENV.fetch('KEYCHAIN_NAME', 'ci'))
end
end
Step 5 — GitHub Actions workflow for build, signing and TestFlight
The workflow runs on macOS, installs Fastlane via Bundler, configures environment variables and executes the lane. The certificates never touch the app repository: Match clones the certificate repo and installs into the temporary keychain. For React Native/Expo apps see the EAS build pipeline at React Native (Expo) CI with GitHub Actions & EAS.
- Use tag triggers to control releases.
- Give jobs minimal permissions.
- Avoid verbose logs that might expose sensitive paths.
name: ios-ci
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: macos-latest
permissions:
contents: read
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
MATCH_GIT_BRANCH: ${{ secrets.MATCH_GIT_BRANCH }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
KEYCHAIN_NAME: ci-keychain
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
steps:
- uses: actions/checkout@v4
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Install dependencies (Fastlane)
run: |
bundle install || (bundle init && echo 'gem "fastlane"' >> Gemfile && bundle install)
- name: Xcode info (sanity check)
run: xcodebuild -version
- name: Build and upload to TestFlight
run: bundle exec fastlane ios ci_appstore
How to confirm it worked and troubleshoot common errors
Confirm in the Actions log that Match cloned the certificate repository, installed the profile, and that the build was uploaded to TestFlight. Check TestFlight in App Store Connect after the job completes. The commands below help diagnose issues on the macOS runner.
For guided manual publishing, see how to publish an iOS app to the App Store.
Common errors and quick tips:
- Incorrect Match password: rotate the password and re-encrypt the certificate repo.
- Keychain locked: increase timeout and ensure unlock: true is set.
- Revoked certificate: regenerate locally and push via Match.
- Insufficient App Store Connect permissions: adjust the user's role.
# Listar identidades de assinatura disponíveis
security find-identity -v -p codesigning
# Verificar perfis instalados
ls ~/Library/MobileDevice/Provisioning\ Profiles || true
# Atualizar Fastlane (se necessário)
bundle update fastlane || bundle exec fastlane update_fastlane
Frequently asked questions
Do I need a macOS runner to build and sign iOS apps?
Yes. Building and signing iOS apps requires Xcode, so you need a macOS environment. On GitHub Actions use macos-latest. You can also use self-hosted macOS runners as long as Xcode matches your project's version.
Is the App Store Connect API key mandatory in CI?
Not strictly mandatory, but recommended. Apple ID authentication requires 2FA and is fragile in non-interactive environments. The App Store Connect API key is stable, rotatable and works well with Fastlane for TestFlight and App Store API calls.
Can I share the certificate repository between multiple apps?
Yes. Match can manage multiple apps in the same repository, organized by identifiers and folders. Still, consider separating by team or project scope. Control access per team and use separate branches to avoid pipeline conflicts.
How do I run Match in readonly mode in CI?
Pass readonly: true when calling match in your Fastlane lane. Generate certificates and profiles locally first and push them to the certificate repository. In CI, Match will only download and install artifacts into the keychain.
What if the Match password or .p8 key is leaked?
Rotate the Match password immediately and re-encrypt the certificate repository. Revoke and create a new .p8 key in App Store Connect. If necessary, regenerate certificates with Match and update provisioning profiles. Store secrets in Environments with required reviews.
How to handle multiple teams in the Apple Developer Program?
Set team_id or team_name in Match/lanes, or use environment variables like FASTLANE_TEAM_ID. Keep a certificate repository per team and limit access to only those who need it to avoid mixing credentials between organizations and products.
Next steps
Want to advance your iOS automation securely? Use this guide as a baseline, harden secrets and environments, and iterate on your pipeline. If you want to discuss your setup, let's talk.
I want to discuss iOS automationSee also
- Tutorial: set up mTLS between Traefik and Docker Compose services using smallstep CA and automatic certificates — step by step
- Tutorial: integrate Sentry into your GitHub Actions deploy pipeline — automatic source map upload & issue creation (PHP & JavaScript)
- Configure retention and compression for Docker container logs with journald + logrotate + rsyslog on a low-disk VPS
- Tutorial: Configure GitHub Actions for integration tests with PostgreSQL in ephemeral containers and automatic migration rollback — a practical guide