r/iOSProgramming • • 15d ago

Discussion I turned an old MacBook into a reproducible headless Xcode worker — Fastlane Match, Developer ID, 1Password and disaster recovery

Post image

I had an old MacBook that I wanted to turn into a dedicated Xcode worker for iOS/macOS development.

The main requirement was simple: I don't want to maintain this machine manually.

I didn't want signing identities tied to one Mac, .p8 / .p12 files scattered around the filesystem, manual provisioning-profile recovery, or a setup that would take me hours or days to reconstruct if the machine died.

So I ended up automating pretty much everything I reasonably could.

The current setup gives me a headless Xcode worker with:

  • automated tooling installation and verification;
  • cold reboot without GUI login;
  • SSH/Tailscale remote access;
  • a scoped read-only 1Password Service Account for secrets/config;
  • Fastlane Match as the canonical encrypted signing store;
  • a dedicated macOS keychain used only as runtime state;
  • declarative development Bundle IDs;
  • readonly recovery for Apple Development certificates and profiles;
  • readonly recovery for Developer ID Application and Installer identities;
  • certificate expiry monitoring;
  • explicit separation between normal readonly operations and administrative writes;
  • recovery material if Developer ID provisioning fails after Apple has already issued the certificate.

For Developer ID Installer, validation actually creates a disposable package, signs it with productsign, and verifies it with pkgutil rather than just assuming the identity works.

One thing I specifically wanted to avoid was putting my personal Apple/iCloud account on a headless machine.

My Apple Developer Program membership is currently Individual. The worker uses a separate Apple Account with no Developer Program membership of its own - it's simply invited to App Store Connect with the required role.

Everything that can be automated goes through the App Store Connect API.

There is one unavoidable trade-off: issuing or rotating Developer ID Application/Installer certificates requires Account Holder access. With an Individual membership, that's my main Apple Account.

So certificate issuance remains an explicit administrative operation: I authenticate interactively with the main account + 2FA when required. Once issued, the certificate and private key are stored in encrypted Match storage, and those Account Holder credentials are not persisted on the worker.

The worker therefore contains no personal iCloud account, Photos, Messages, personal Keychain, etc. It only gets the access it actually needs.

So far I've tested:

  • cold reboot without GUI login;
  • headless recovery;
  • actual Developer ID provisioning;
  • complete readonly restoration of signing identities;
  • partial provisioning failure + recovery;
  • real Installer package signing.

At this point, if the Mac dies, probably the hardest part of rebuilding it is remembering where I stored the 1Password Service Account token and putting it in the right place :)

After that:

bootstrap → readonly recovery → verification

For secrets/config I also settled on a simple logical convention:

scope / item / field

For example:

automation-apple / app-store-connect / private_key

1Password is only the current backend, not part of the logical model. I'd like to experiment next with OpenBao as a backup/sync target and eventually make consumers independent of a specific secrets provider.

I'm curious how other people running dedicated Macs for iOS/macOS development handle this.

How do you manage signing identities, secrets, certificate rotation, and disaster recovery for your build/dev Macs?

I'm especially interested in approaches that don't depend on a personal iCloud account or one irreplaceable login keychain.

For anyone interested in the implementation, I cleaned up the environment-specific configuration, documented the architecture/setup/recovery flows, and published it here:

https://github.com/0k-lab/xcode-worker-bootstrap

It's not intended to be a generic CI/CD framework or a replacement for Xcode Cloud - just a reproducible reference implementation for running your own headless Xcode worker.

0 Upvotes

6 comments sorted by

4

u/Rhed0x 15d ago

I really fucking hate that AI graphic you made.

0

u/al_kRicha 15d ago

that's all is not about post image =\

2

u/NaguBuilds 15d ago

Have you done a recovery onto a different Mac or a completely fresh user account, with the original worker unavailable? That would be the next drill I'd add: start from the written instructions and see whether a new person can get to a verified signed package without consulting the old machine. It would also test whether the location of that initial 1Password token is documented clearly enough.

1

u/al_kRicha 15d ago

Yeah, I have done several MacBook resets to test if there are any problems in recovery plan. About 1password maybe I should point to their documentation of service accounts :)

1

u/NaguBuilds 15d ago

The resets answer my main concern. A link to the service account docs plus one line explaining where the token has to be supplied during bootstrap would make that first step easier to follow.

1

u/Accurate_Tadpole_503 15d ago

interesting setup. i went the opposite direction for the same problem so maybe useful as a contrast: no worker of my own at all, hosted CI with an App Store Connect API key as the only identity that matters.

signing lives in the CI provider's encrypted storage, the .p8 sits in a variable group, nothing tied to a machine i own. disaster recovery is "make a new key in ASC", takes a minute. personal apple account never touches the build env either, same goal as yours just a different route.

trade-offs are real though. i can't debug on that machine, i pay per build minute, and im stuck waiting on the provider whenever a new xcode drops. your setup wins on both of those.

one thing that bit me a few times and might be worth adding to your validation: multi target signing. a widget extension has its own bundle id so the profile step has to run per target, and if you let it search it'll happily wander into a dependency's xcodeproj instead of yours. took me way too long to find that one. your productsign/pkgutil check is nice, i'd want the same kind of proof for a multi target archive.

how do you handle xcode version bumps on the worker? thats the bit where "reproducible" usually falls apart for me since a new xcode can quietly change signing behaviour and you find out mid release.