What keychain setup does match need on a macOS CI runner to install iOS signing certificates?
answer
- no login session on a runner
- dedicated keychain, created and unlocked
- keychain_name and keychain_password
- partition list stops the hidden prompt
- delete the keychain after the job
basics
~20 smatch imports the private key into a macOS keychain, so a CI runner needs a dedicated keychain created, unlocked and kept in the search list for the whole build, named to sync_code_signing through keychain_name and keychain_password.
solid answer
~40 sSigning material lives in a macOS keychain, and a CI runner has no interactive login session to unlock one. The reliable pattern for the falconry weight-log iOS build is: create a **dedicated keychain** for the job, unlock it, put it in the search list, and give it a lock timeout that outlives the build; then pass `keychain_name` and `keychain_password` to `sync_code_signing` so match imports the certificate there rather than into the login keychain. match also sets the imported key's partition list so the signing step can use it without an interactive authorisation prompt — the thing that otherwise hangs a headless job with no output — unless you set `skip_set_partition_list`. On a long-lived self-hosted Mac, delete that keychain when the job finishes.
code
bash · 6 linesKEYCHAIN=falconry-ci.keychain-db
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security list-keychains -d user -s "$KEYCHAIN" login.keychain-db
bundle exec fastlane sync_signinggo deeper
Know that the certificate match installs must land in a macOS keychain, and that on CI that keychain has to be created and unlocked by the job because nobody logs in.
Be able to walk the sequence — create, set lock timeout, unlock, add to search list, pass keychain_name and keychain_password to match — and say why each step exists.
Diagnose the silent hang: an authorisation prompt no one can answer, the partition list that prevents it, a keychain re-locking mid-archive, and cleanup on shared runners.
Decide the runner strategy this implies — ephemeral macOS capacity versus a maintained Mac fleet — and how signing setup is standardised so every team is not reinventing it.
## Why the keychain is where iOS CI signing breaks On macOS a private key is not a file the build reads; it is an entry in a **keychain**, an encrypted store that must be unlocked before anything can use what is inside it. On a laptop this is invisible: logging in unlocks the login keychain, and the signing step finds the key. A CI runner has no login session, no desktop and nobody to click a button, so every one of those implicit steps has to be made explicit. This is why iOS signing is the classic mobile CI failure, and why `sync_code_signing` (the canonical action behind `match`) exposes `keychain_name` and `keychain_password` at all: fetching and decrypting the assets is the easy half; getting the private key into a store the build can actually use is the half that fails. ## The setup, in order 1. **Create a keychain for the job.** Not the login keychain — a named one, with a password the job generates or reads from the pipeline's secret store. 2. **Set its lock settings.** Give it a timeout longer than the whole build. A keychain that auto-locks after the default idle period will re-lock in the middle of a long archive step, and the signing step then fails for a reason that looks nothing like a keychain problem. 3. **Unlock it.** 4. **Put it in the search list** so tools that look up an identity by name actually see it. 5. **Run match against it** by passing `keychain_name` and `keychain_password`, so the certificate and private key are imported there. 6. **Delete it when the job ends** — mandatory on a shared runner, harmless on an ephemeral one. ## What you hand to sync_code_signing `keychain_name` selects the destination and `keychain_password` is what match uses to unlock it before importing. Everything else in the lane is unchanged: `storage_mode` and its backend options say where the encrypted assets come from, `app_identifier` and `type` say which ones, and `readonly` should be true on a runner. The keychain options are purely about the *destination on this machine*, which is why they are so easy to forget until the first headless run. ## The invisible prompt The subtlest failure is a build that stops producing output and eventually hits the job timeout. What happened is that the signing step asked macOS for permission to use the private key, macOS raised an authorisation prompt, and there is no human and no window server session to answer it. The key's **partition list** is the access-control data that decides whether a tool may use the key without asking. match sets it as part of the import, which is why the option to *not* do that is spelled `skip_set_partition_list`. Treat that option as a diagnostic escape hatch, not a fix: turning it on to make an error message go away is how you get the hang instead. A short triage list for a hung or failing signing step on a runner: - Is the job's keychain unlocked *at the moment of signing*, not merely at the start of the job? - Is it in the search list, and is it the keychain match actually imported into? - Did anything set `skip_set_partition_list`? - Is the certificate the one you think it is — same `type`, same `app_identifier`, and not one left over from a previous job? ## Ephemeral versus persistent runners | | ephemeral runner | long-lived self-hosted Mac | |---|---|---| | starting state | clean image, no keychain, no identities | whatever the last job left behind | | main risk | forgetting setup entirely | inheriting a stale certificate or a locked keychain | | cleanup | free — the machine is destroyed | mandatory: delete the job keychain explicitly | | debugging | reproducible, because every run starts equal | 'works on runner 2, fails on runner 5' | The practical rule is to make the persistent Mac behave like an ephemeral one: create, unlock, use, delete, every job, with a unique keychain name so two concurrent jobs on the same machine cannot fight over one store. ## The Android half of the picture There is no equivalent step on Android, and this asymmetry is worth saying out loud in an interview rather than letting the interviewer assume you have not noticed it. Android signing uses a keystore file, and reading a file needs no unlocking daemon, no search list and no partition list — so an Android job on the same runner needs none of this ceremony. Everything above is Apple-specific, which is also why it never appears in a Linux build container: `sync_code_signing` needs macOS because the keychain does. One last boundary: where the keychain password and the match passphrase come from is a CI secrets question, not a match one. What belongs to this topic is what the runner must *do* with them once they arrive.
- Your iOS CI job hangs at the signing step with no further output. What do you check first?An invisible keychain authorisation prompt: macOS asked permission to use the private key and nobody can answer. Confirm the job's keychain is unlocked at that moment, that its lock timeout outlives the build, that match set the key's partition list — so `skip_set_partition_list` is not on — and that the identity was imported into that keychain rather than the login one.
- Why create a separate keychain instead of using the runner's login keychain?The login keychain is unlocked by an interactive login the job does not have, it survives between jobs and accumulates identities from other builds, and its password belongs to a human. A per-job keychain is created, unlocked, used and deleted, so runs cannot inherit each other's signing state.
- What changes between an ephemeral runner and a long-lived self-hosted Mac?On an ephemeral runner the keychain dies with the machine, so setup is all that matters. On a shared Mac the keychain and any leftover identities persist, so the lane must delete its keychain in a cleanup step; otherwise a later job can sign with a stale certificate or trip over a locked store.
saying these in an interview costs you the question
- Imports certificates into the runner's login keychain
- Thinks an unlocked keychain never re-locks mid-build
- Blames a hung signing step on the network
- Leaves the CI keychain behind on a self-hosted Mac
- Sets skip_set_partition_list to silence an error
- Assumes an Android job needs the same keychain setup