Einleitung
Führst du den Release-Prozess deiner iOS-App noch jedes Mal manuell über Archive → Distribute... in Xcode durch?
Dieser Prozess ist zeitaufwendig und führt leicht zu umgebungsabhängigen Builds – bekannt als das Problem: "Bei mir baut es, aber auf dem anderen Mac nicht."
In diesem Artikel erklären wir, wie du eine vollständige Pipeline zum Bauen, Signieren und automatischen Hochladen einer iOS-App zu TestFlight mit GitHub Actions aufbaust.
Viele Tutorials zeigen eine Konfiguration mit fastlane, aber in diesem Artikel verwenden wir ausschließlich Apples eigenes xcodebuild und xcrun altool – ganz ohne fastlane. So wird keine Ruby-Umgebung benötigt, der Workflow bleibt einfach, und du bist von fastlane-Kompatibilitätsproblemen bei Xcode-Updates befreit.
Als Authentifizierungsmethode verwenden wir statt Apple ID + Passwort den sichereren App Store Connect API-Key, der nicht von der Zwei-Faktor-Authentifizierung betroffen ist.
Voraussetzungen
- Mitgliedschaft im Apple Developer Program (kostenpflichtig)
- App-Eintrag in App Store Connect erstellt
- iOS-Projekt in ein GitHub-Repository gepusht
- Lokale Builds unter macOS / Xcode erfolgreich
Gesamtablauf
Der Überblick über die aufzubauende Pipeline sieht wie folgt aus:
- Workflow wird durch Push auf den
main-Branch oder manuell ausgelöst - Xcode auf dem GitHub Actions macOS-Runner vorbereiten
- Zertifikat und Provisioning Profile temporär in den Keychain importieren
- Build-Nummer aktualisieren
.xcarchivemitxcodebuild archiveerstellen.ipamitxcodebuild -exportArchiveexportieren- Mit
xcrun altoolzu TestFlight hochladen - Temporären Keychain löschen und aufräumen
push → Actions gestartet → Zertifikat installieren → archive → export → altool → TestFlight
Step 1: Zertifikat und Provisioning Profile vorbereiten
Für Code Signing in CI werden die folgenden zwei Dateien Base64-kodiert und in GitHub Secrets gespeichert.
1-1. Verteilungszertifikat (.p12) exportieren
Öffne "Schlüsselbundverwaltung.app" auf deinem lokalen Mac, klicke mit der rechten Maustaste auf das Apple Distribution- oder iPhone Distribution-Zertifikat und exportiere es im .p12-Format. Notiere das beim Export gesetzte Passwort – du wirst es später benötigen.
Falls du kein Zertifikat hast, erstelle eines unter Certificates auf der Apple Developer-Website.
1-2. Provisioning Profile (.mobileprovision) herunterladen
Lade das Provisioning Profile für die App Store-Verteilung unter Profiles auf der Apple Developer-Website herunter. Notiere dabei unbedingt auch den Profilnamen (z.B. YourApp AppStore). Du wirst ihn später in der ExportOptions.plist benötigen.
1-3. In Base64 kodieren
Führe folgende Befehle im Terminal aus, um die Dateien in Base64-Strings umzuwandeln.
# Zertifikat
base64 -i Certificates.p12 | pbcopy
# Provisioning Profile
base64 -i YourApp_AppStore.mobileprovision | pbcopy
Durch pbcopy wird der Inhalt in die Zwischenablage kopiert und kann direkt in GitHub Secrets eingefügt werden.
Step 2: App Store Connect API-Key generieren
Um Probleme mit der Zwei-Faktor-Authentifizierung zu vermeiden, verwenden wir einen API-Key.
- Bei App Store Connect anmelden
- "Benutzer und Zugriff" → "Integrations" → "App Store Connect API" auswählen
- Mit der "+"-Schaltfläche einen Key generieren (Rolle muss App Manager oder höher sein)
- Die heruntergeladene
AuthKey_XXXXXXXXXX.p8-Datei speichern (kann nicht erneut heruntergeladen werden – Vorsicht!) - Die bei der Erstellung angezeigte Key ID und Issuer ID notieren
Die .p8-Datei ebenfalls in Base64 kodieren:
base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy
Step 3: In GitHub Secrets registrieren
Unter Settings → Secrets and variables → Actions im Repository folgende Secrets registrieren:
| Secret-Name | Inhalt |
|---|---|
BUILD_CERTIFICATE_BASE64 |
Base64-String des .p12-Zertifikats |
P12_PASSWORD |
Beim .p12-Export gesetztes Passwort |
BUILD_PROVISION_PROFILE_BASE64 |
Base64-String der .mobileprovision |
KEYCHAIN_PASSWORD |
Beliebiges Passwort für den temporären Keychain |
APP_STORE_CONNECT_API_KEY_ID |
Key ID des API-Keys |
APP_STORE_CONNECT_API_ISSUER_ID |
Issuer ID |
APP_STORE_CONNECT_API_KEY_BASE64 |
Base64-String der .p8 |
Step 4: ExportOptions.plist erstellen
Diese Konfigurationsdatei wird benötigt, um aus einer .xcarchive eine .ipa zu erzeugen. Sie wird als ios/ExportOptions.plist ins Repository committed (enthält keine vertraulichen Informationen).
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>ABCD123456</string>
<key>signingStyle</key>
<string>manual</string>
<key>signingCertificate</key>
<string>Apple Distribution</string>
<key>provisioningProfiles</key>
<dict>
<key>com.example.yourapp</key>
<string>YourApp AppStore</string>
</dict>
<key>uploadSymbols</key>
<true/>
<key>uploadBitcode</key>
<false/>
</dict>
</plist>
Step 5: GitHub Actions Workflow erstellen
Jetzt geht es ans Eingemachte. Erstelle .github/workflows/ios-testflight.yml.
name: iOS TestFlight Deploy
on:
push:
branches: [main]
workflow_dispatch:
env:
SCHEME: YourApp
WORKSPACE: YourApp.xcworkspace # Bei .xcodeproj stattdessen PROJECT verwenden
CONFIGURATION: Release
EXPORT_OPTIONS_PLIST: ios/ExportOptions.plist
jobs:
deploy:
runs-on: macos-14
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.4.app
- name: Install the Apple certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# Base64 dekodieren
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
# Temporären Keychain erstellen
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Zertifikat importieren
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# Provisioning Profile platzieren
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
- name: Set build number
run: |
# GitHub Actions Run-Nummer als Build-Nummer verwenden (Eindeutigkeit garantiert)
BUILD_NUMBER=${{ github.run_number }}
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" \
YourApp/Info.plist
echo "Build number set to $BUILD_NUMBER"
- name: Build archive
run: |
xcodebuild archive \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-archivePath "$RUNNER_TEMP/YourApp.xcarchive" \
-destination "generic/platform=iOS" \
-allowProvisioningUpdates \
CODE_SIGNING_ALLOWED=YES \
| xcbeautify
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/YourApp.xcarchive" \
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST" \
-exportPath "$RUNNER_TEMP/export" \
| xcbeautify
- name: Prepare API key for altool
env:
APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
run: |
# altool liest aus ~/.appstoreconnect/private_keys/ oder ./private_keys/
mkdir -p ~/.appstoreconnect/private_keys
echo -n "$APP_STORE_CONNECT_API_KEY_BASE64" | base64 --decode \
-o ~/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8
- name: Upload to TestFlight
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
run: |
xcrun altool --upload-app \
--type ios \
--file "$RUNNER_TEMP/export/YourApp.ipa" \
--apiKey "$APP_STORE_CONNECT_API_KEY_ID" \
--apiIssuer "$APP_STORE_CONNECT_API_ISSUER_ID"
- name: Clean up keychain and profiles
if: always()
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
rm -f ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision
rm -rf ~/.appstoreconnect/private_keys
Wichtige Hinweise
github.run_number als Build-Nummer verwenden
Statt fastlanes increment_build_number nutzen wir die von GitHub Actions automatisch vergebene Ausführungsnummer. Diese ist monoton steigend und erfüllt TestFlights Anforderung "keine doppelten Build-Nummern". Alternativ funktioniert auch eine datumsbasierte Nummer ($(date +%Y%m%d%H%M)).
Logs mit xcbeautify formatieren
Die Rohlogs von xcodebuild sind sehr schwer lesbar, daher leiten wir sie durch xcbeautify. Auf macos-14-Runnern ist es vorinstalliert. Falls nicht vorhanden, füge brew install xcbeautify als Schritt hinzu.
Speicherort der API-Key-Datei
xcrun altool erkennt den API-Key automatisch aus einem der folgenden Pfade. Da kein Flag zur expliziten Pfadangabe existiert, muss die Datei in einem der vorgesehenen Verzeichnisse abgelegt werden:
./private_keys/AuthKey_<KEY_ID>.p8~/private_keys/AuthKey_<KEY_ID>.p8~/.private_keys/AuthKey_<KEY_ID>.p8~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8
Warum einen temporären Keychain verwenden
Zertifikate in den Standard-Keychain der CI-Umgebung zu importieren ist umständlich aufzuräumen und sicherheitstechnisch bedenklich. Pro Job einen Keychain zu erstellen und danach zu löschen ist Best Practice.
Aufräumen mit if: always()
Auch bei einem fehlgeschlagenen Build werden der temporäre Keychain und die Schlüsseldateien immer gelöscht. Das ist besonders bei self-hosted Runnern wichtig.
Step 6: Funktionsprüfung
Push auf den main-Branch oder führe den Workflow manuell über den Actions-Tab mit Run workflow aus.
Nach etwa 5–10 Minuten ist der Vorgang abgeschlossen und ein neuer Build erscheint im TestFlight-Tab von App Store Connect. Der Wechsel vom Status "In Bearbeitung" (Processing) zur Fertigstellung kann weitere 10–30 Minuten dauern.
Zertifikatsverwaltung: Optionen und Empfehlungen
Wir haben die Pipeline mit der Methode aufgebaut, .p12 in Base64 zu kodieren und in GitHub Secrets zu speichern, aber GitHub Actions bietet weitere Optionen zur Zertifikatsverwaltung. Wähle je nach Teamgröße und Anforderungen die passende Option.
Option 1: Als Base64 in Secrets speichern (Methode dieses Artikels)
Funktionsweise: .p12 und .mobileprovision werden in Base64 kodiert und als Strings in GitHub Secrets gespeichert.
Vorteile
- Keine externen Abhängigkeiten, alles innerhalb von GitHub
- Einfachste Ersteinrichtung
.p12-Dateien sind meist wenige KB bis einige Dutzend KB und liegen deutlich unter dem 64-KB-Limit von Secrets
Nachteile
- Bei Zertifikatsrotation manuelle Aktualisierung der Secrets nötig
- Bei mehreren Apps steigt die Anzahl der Secrets und wird unübersichtlich
- Secrets können nicht wie Azure DevOps Secure Files als Dateien behandelt werden
Geeignet für: Einzelentwickler, kleine Teams mit 1–3 Personen, einzelne App
Option 2: Verschlüsselte Dateien ins Repository committen
Funktionsweise: .p12 wird mit GPG oder OpenSSL verschlüsselt und ins Repository committet; nur die Passphrase wird in Secrets gespeichert.
# Lokal verschlüsseln
gpg --symmetric --cipher-algo AES256 Certificates.p12
# → Certificates.p12.gpg ins Repository committen
Im Workflow:
- name: Decrypt certificate
run: |
gpg --quiet --batch --yes --decrypt \
--passphrase="$CERT_PASSPHRASE" \
--output $RUNNER_TEMP/cert.p12 \
secrets/Certificates.p12.gpg
env:
CERT_PASSPHRASE: ${{ secrets.CERT_PASSPHRASE }}
Vorteile
- Als Datei verwaltbar; Rotationsverlauf über Git-Historie nachvollziehbar
- Keine Secrets-Größenbeschränkung
- Nächste Analogie zu Azure DevOps Secure Files
Nachteile
- Bei Repository-Kompromittierung ist die Passphrase-Stärke die letzte Verteidigungslinie
.gpg-Dateien im Repository können das Erscheinungsbild beeinflussen
Geeignet für: Migration von Azure DevOps, dateibasierte Verwaltung gewünscht, private Repositories
Option 3: fastlane match
Funktionsweise: Verschlüsselte Zertifikate werden in einem dedizierten privaten Repository (oder S3/GCS) gespeichert; fastlane übernimmt automatisch Abrufen, Installieren und Entschlüsseln.
Vorteile
- Zertifikate für mehrere Apps und Umgebungen (dev/adhoc/appstore) zentral verwalten
- Neue Teammitglieder richten die lokale Umgebung mit einem einzigen
fastlane match-Aufruf ein - Automatische Zertifikatsgenerierung unterstützt
Nachteile
- Ruby und fastlane erforderlich
- Mögliche fastlane-Kompatibilitätsprobleme bei Xcode-Updates
- Widerspricht dem fastlane-freien Ansatz dieses Artikels
Geeignet für: Mehrere Apps, iOS-Teams ab 5 Personen, häufige Zertifikatsrotation
Option 4: AWS Parameter Store + OIDC-Integration
Funktionsweise: Base64-Strings von .p12 und Passwörter werden als SecureString im AWS Systems Manager Parameter Store gespeichert und über OIDC aus GitHub Actions abgerufen. Kein Bedarf an langlebigen Anmeldeinformationen – ein großer Vorteil.
Parameter Store vs. Secrets Manager
AWS bietet zwei ähnliche Dienste, hier ein Überblick:
| Eigenschaft | Parameter Store | Secrets Manager |
|---|---|---|
| Kosten (Standard) | Kostenlos | $0,40/Secret/Monat + API-Gebühren |
| Kosten (Advanced) | $0,05/10.000 API-Aufrufe | Wie oben |
| Wertgröße | Standard 4 KB / Advanced 8 KB | 64 KB |
| Automatische Rotation | Nein | Ja |
| KMS-Verschlüsselung | Via SecureString-Typ | Nativ |
Base64-kodierte .p12-Dateien haben oft einige Dutzend bis 30 KB, was die entscheidende Weiche ist. Wenn es in 8 KB passt, ist Parameter Store deutlich günstiger; überschreitet es das, sind Secrets Manager oder der unten beschriebene S3-Hybridansatz besser geeignet.
Umgang mit Dateien über 8 KB
Ist die Größe zu groß, ist eine Hybridkonfiguration praktisch elegant: Die verschlüsselte .p12 wird in S3 abgelegt, und Parameter Store enthält nur den S3-Schlüssel und das Entschlüsselungspasswort. Damit wird die Einfachheit von Parameter Store mit der Größenfreiheit von S3 kombiniert.
AWS-Einrichtung
Erstelle zunächst einen OIDC-Identitätsanbieter für GitHub Actions in IAM (überspringen, falls bereits vorhanden):
- Provider URL:
https://token.actions.githubusercontent.com - Audience:
sts.amazonaws.com
Dann Parameter im Parameter Store speichern (AWS CLI-Beispiel):
# Zertifikatskörper
aws ssm put-parameter \
--name /ios/yourapp/dist-cert-base64 \
--type SecureString \
--value "$(base64 -i Certificates.p12)"
# .p12-Passwort
aws ssm put-parameter \
--name /ios/yourapp/p12-password \
--type SecureString \
--value "your-p12-password"
# Provisioning Profile
aws ssm put-parameter \
--name /ios/yourapp/provisioning-profile-base64 \
--type SecureString \
--value "$(base64 -i YourApp_AppStore.mobileprovision)"
# App Store Connect API-Key (.p8)
aws ssm put-parameter \
--name /ios/yourapp/asc-api-key-base64 \
--type SecureString \
--value "$(base64 -i AuthKey_XXXXXXXXXX.p8)"
Eine IAM-Rolle für GitHub Actions erstellen und in der Trust Policy auf spezifische Repositories und Branches beschränken:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
}
}
}
]
}
Berechtigungsrichtlinie nach Least-Privilege-Prinzip – nur GetParameter für den Zielpfad:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ssm:GetParameter", "ssm:GetParameters"],
"Resource": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/ios/yourapp/*"
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id"
}
]
}
GitHub Actions Workflow
Vergiss nicht, id-token: write im permissions-Block zu setzen. Ohne diesen Eintrag kann kein OIDC-Token abgerufen werden.
permissions:
id-token: write # Für OIDC-Token-Abruf erforderlich
contents: read
jobs:
deploy:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-ios-deploy
aws-region: ap-northeast-1
- name: Fetch secrets from Parameter Store
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# Aus Parameter Store abrufen (SecureString benötigt --with-decryption)
aws ssm get-parameter \
--name /ios/yourapp/dist-cert-base64 \
--with-decryption \
--query Parameter.Value --output text | base64 --decode > $CERTIFICATE_PATH
aws ssm get-parameter \
--name /ios/yourapp/provisioning-profile-base64 \
--with-decryption \
--query Parameter.Value --output text | base64 --decode > $PP_PATH
P12_PASSWORD=$(aws ssm get-parameter \
--name /ios/yourapp/p12-password \
--with-decryption \
--query Parameter.Value --output text)
# Keychain erstellen und Zertifikat importieren
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# Folgende archive / export / altool-Schritte sind identisch mit Step 5
Vorteile
- Nahezu keine Kosten: Standard-Parameter sind vollständig kostenlos; Advanced ist extrem günstig
- Keine langlebigen Anmeldeinformationen dank OIDC: Kein Access-Key in GitHub notwendig (größter Vorteil)
- Audit-Logs mit CloudTrail: Vollständige Aufzeichnung, wer wann auf das Zertifikat zugegriffen hat
- Feingranulare Berechtigungen mit IAM/KMS: Getrennte Rollen für Entwicklungs- und Produktionsumgebung
- Auf mehreren CI/CD-Systemen nutzbar: Gleiches Zertifikat aus GitHub Actions, CodeBuild, Jenkins und lokaler Entwicklung
- Versionsverlauf: Rollback auf frühere Versionen möglich
Nachteile
- AWS-Konto erforderlich (hoher Einrichtungsaufwand für Teams, die AWS noch nicht nutzen)
- Workaround nötig, wenn
.p128 KB überschreitet (S3 kombinieren oder Secrets Manager) - Gewisse Lernkurve für OIDC-Einrichtung (nur einmalig)
Geeignet für: Teams, die AWS bereits nutzen; Audit-Log-Anforderungen; gleiches Zertifikat über mehrere CI/CD-Tools; Enterprise-Qualitätsmanagement zu niedrigen Kosten
Option 5: AWS Secrets Manager + OIDC-Integration
Funktionsweise: .p12 wird direkt als Binärdatei (SecretBinary) im AWS Secrets Manager gespeichert und über OIDC aus GitHub Actions abgerufen. Im Gegensatz zu Parameter Store können Dateien direkt behandelt werden, ohne sich um Base64-Konvertierung sorgen zu müssen.
Unterschiede zu Parameter Store
| Eigenschaft | Parameter Store | Secrets Manager |
|---|---|---|
| Speicherformat | Nur Strings (Binär erfordert Base64) | Binär direkt möglich (SecretBinary-Typ) |
| Größenlimit | 4 KB (Standard) / 8 KB (Advanced) | 64 KB |
| Kosten | Kostenlos bis sehr günstig | $0,40/Monat/Secret + API-Gebühren |
| Automatische Rotation | Nein | Ja (Lambda-Integration) |
| Verwendungszweck | Konfigurationswerte, kleine Geheimnisse | Größere Geheimnisse, Produktionsbetrieb |
Wenn .p12 8 KB überschreitet oder dateibasierte Handhabung bevorzugt wird, ist Secrets Manager die praktischere Wahl. Bei 1–2 Zertifikaten liegen die monatlichen Kosten unter einem Dollar.
AWS-Einrichtung
Die OIDC-Identitätsanbieter-Einrichtung ist mit Option 4 identisch und wird hier übersprungen.
Mit dem fileb://-Präfix als Binärdatei registrieren; die AWS CLI sendet die Datei direkt als Binärdaten:
# Zertifikatskörper (direkt als Binärdatei speicherbar)
aws secretsmanager create-secret \
--name ios/yourapp/dist-cert \
--secret-binary fileb://Certificates.p12
# Provisioning Profile als Binärdatei
aws secretsmanager create-secret \
--name ios/yourapp/provisioning-profile \
--secret-binary fileb://YourApp_AppStore.mobileprovision
# App Store Connect API-Key (.p8) als Binärdatei
aws secretsmanager create-secret \
--name ios/yourapp/asc-api-key \
--secret-binary fileb://AuthKey_XXXXXXXXXX.p8
# Passwörter als String
aws secretsmanager create-secret \
--name ios/yourapp/p12-password \
--secret-string "your-p12-password"
Für Updates update-secret verwenden:
aws secretsmanager update-secret \
--secret-id ios/yourapp/dist-cert \
--secret-binary fileb://Certificates_new.p12
Die IAM-Rollen-Berechtigungsrichtlinie erlaubt Secrets Manager statt Parameter Store:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:ios/yourapp/*"
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id"
}
]
}
GitHub Actions Workflow
Über SecretBinary abgerufene Werte kommen Base64-kodiert in der API-Antwort zurück, daher mit base64 --decode dekodieren und in eine Datei schreiben:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-ios-deploy
aws-region: ap-northeast-1
- name: Fetch certificates from Secrets Manager
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# SecretBinary kommt Base64-kodiert zurück; dekodieren und in Datei schreiben
aws secretsmanager get-secret-value \
--secret-id ios/yourapp/dist-cert \
--query SecretBinary --output text | base64 --decode > $CERTIFICATE_PATH
aws secretsmanager get-secret-value \
--secret-id ios/yourapp/provisioning-profile \
--query SecretBinary --output text | base64 --decode > $PP_PATH
# SecretString kann direkt abgerufen werden
P12_PASSWORD=$(aws secretsmanager get-secret-value \
--secret-id ios/yourapp/p12-password \
--query SecretString --output text)
# API-Key (.p8) am von altool erwarteten Pfad ablegen
API_KEY_ID="${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}"
mkdir -p ~/.appstoreconnect/private_keys
aws secretsmanager get-secret-value \
--secret-id ios/yourapp/asc-api-key \
--query SecretBinary --output text | base64 --decode \
> ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8
# Keychain erstellen und Zertifikat importieren
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# Folgende archive / export / altool-Schritte sind identisch mit Step 5
Vorteile
.p12direkt als Datei speichern und abrufen (größte Analogie zu Azure DevOps Secure Files)- Unterstützung bis 64 KB: Keine Sorgen um Größenlimits
- Keine langlebigen Anmeldeinformationen dank OIDC: Gleiche Sicherheitsvorteile wie Option 4
- Audit-Logs mit CloudTrail standardmäßig verfügbar
- Automatische Rotation: Zertifikate können via Lambda automatisch erneuert werden (Apple-seitige API-Integration erhöht jedoch den Aufwand)
- Feingranulare Berechtigungen mit IAM
Nachteile
- Kostenverursachend: ~$0,40/Secret/Monat + API-Gebühren; bei 3–5 Zertifikaten wenige Dollar monatlich
- AWS-Konto erforderlich
- Lernkurve für OIDC-Einrichtung (geteilt mit Option 4)
Geeignet für: Dateibasierte Handhabung bevorzugt; große .p12-Dateien; viele Zertifikate über mehrere Apps; zukünftige automatische Rotation angestrebt; Azure DevOps Secure Files-ähnliche Erfahrung gewünscht
Option 6: Weitere Cloud-Secret-Manager
Für Teams, die kein AWS nutzen, sind folgende Optionen nach dem gleichen Prinzip verwendbar. Alle unterstützen OIDC-Integration:
| Dienst | Kosten | Größenlimit | Binärunterstützung |
|---|---|---|---|
| Azure Key Vault | $0,03/10.000 Ops | 25 KB | ○ (Certificate-Typ) |
| Google Secret Manager | $0,06/Monat/Secret | 64 KB | ○ |
| HashiCorp Vault (OSS) | Nur Serverkosten | Keine Begrenzung | ○ |
Azure Key Vault bietet insbesondere einen Certificate-Typ mit offizieller Unterstützung für direkten .p12-Import/-Export – ideal für Azure-basierte Teams.
Option 7: Zusätzlicher Schutz durch GitHub Environments
Diese Option kann mit allen obigen kombiniert werden. Durch GitHub Environments lässt sich der Zertifikatszugriff auf Deployments von bestimmten Branches beschränken oder ein manueller Genehmigungsschritt eingebaut werden:
jobs:
deploy:
runs-on: macos-14
environment: production # ← Secrets dieses Environments können genutzt werden
Da Secrets pro Environment isoliert werden können, wird verhindert, dass Produktions-Zertifikate versehentlich für Entwicklungs-Builds verwendet werden.
Empfehlungen nach Teamgröße
| Größe | Empfohlene Konfiguration |
|---|---|
| Einzelentwickler / Hobbyprojekt | Option 1 (Base64 + Secrets) – Einfachheit gewinnt |
| Startup / kleines Team (1–3 Personen) | Option 1 oder Option 2 (verschlüsselte Dateien) |
| Mittelgroßes Team (mehrere Apps oder 5+ Personen) | Option 3 (fastlane match) oder Option 5 (Secrets Manager) |
| AWS-Nutzer, kostenbewusst | Option 4 (Parameter Store + OIDC) |
| AWS-Nutzer, betriebsorientiert | Option 5 (Secrets Manager + OIDC) – dateifreundlich |
| Enterprise / Audit-Anforderungen | Option 4 oder 5 + Option 7 (Environments) kombiniert |
| Migration von Azure DevOps | Option 2 (verschlüsselte Dateien) oder Option 5 (Secrets Manager) – nächste Analogie zu Secure Files |
Häufige Probleme und Lösungen
No signing certificate "iOS Distribution" found
Tritt häufig auf, wenn der Zertifikatsimport in den Keychain fehlgeschlagen ist oder set-key-partition-list fehlt. Füge security find-identity -v -p codesigning $KEYCHAIN_PATH als Schritt ein, um zu prüfen, ob das Zertifikat sichtbar ist – das erleichtert die Fehlersuche.
error: exportArchive: "YourApp.app" requires a provisioning profile
Das Provisioning Profile wurde nicht korrekt platziert, oder Bundle ID und Profilname in provisioningProfiles in der ExportOptions.plist stimmen nicht überein. Beachte: Der Profilname ist der "auf der Apple Developer-Website angezeigte Name", nicht der Dateiname.
altool: Invalid API key
Oft verursacht durch Zeilenumbrüche oder zusätzliche Leerzeichen in der Base64-kodierten .p8-Datei. Generiere sie mit base64 -i file.p8 | pbcopy und füge sie direkt ein. Wenn der Dateiname nicht der Namenskonvention AuthKey_<KEY_ID>.p8 folgt, erkennt altool ihn nicht – stelle ihn daher präzise über Umgebungsvariablen zusammen.
xcodebuild: error: The operation couldn't be completed. No such file or directory
Tritt auf, wenn .xcworkspace verwendet wird, aber -project angegeben ist (oder umgekehrt). Bei Verwendung eines CocoaPods- oder Swift Package Manager-Workspaces -workspace angeben.
Parameter Store: ParameterNotFound oder AccessDenied
Fehler bei der AWS-Integration. In dieser Reihenfolge prüfen:
- Tippfehler im Parameterpfad (Hierarchie wie
/ios/yourapp/...)? - Ist
ssm:GetParameterfür den Zielpfad in der IAM-Rollen-Berechtigungsrichtlinie erlaubt? - Bei SecureString: Wird das
--with-decryption-Flag verwendet? - Hat die IAM-Rolle
kms:Decryptfür den KMS-Schlüssel? - Stimmt die
sub-Bedingung in der Trust Policy mit dem Workflow-Ausführungs-Branch überein?
Weitere Verbesserungen
Wir haben die Pipeline mit einer Minimalfonfiguration aufgebaut. Folgende Erweiterungen sind denkbar:
- Slack-Benachrichtigungen:
slackapi/slack-github-actionintegrieren, um automatisch zu benachrichtigen, wenn der Upload abgeschlossen ist. - Automatische Release Notes: Änderungen aus
git logextrahieren und automatisch in das "What to Test"-Feld von TestFlight eintragen (mit altool allein schwierig; erfordert direkten Aufruf der App Store Connect API). - Tag-Push-Trigger: Statt Push auf
mainbeiv*-Tag-Push auslösen für einen bewussteren Release-Flow. - Unit-Tests vorab ausführen:
xcodebuild testvor dem Archive ausführen und bei Testfehler das Deployment stoppen.
Fazit
Eine fastlane-freie Konfiguration hat die Vorteile, intuitiv und leicht verständlich zu sein, keine Ruby-Umgebung zu benötigen und von fastlane-Kompatibilitätsproblemen bei Xcode-Updates unabhängig zu sein. Dafür müssen Dinge wie die Build-Nummer-Verwaltung selbst übernommen werden, und beim Betrieb mehrerer Apps steigt der Verwaltungsaufwand. In diesem Fall ist eine Migration zu fastlane match oder AWS Secrets Manager / Parameter Store sinnvoll.
Die größte Hürde bei der Ersteinrichtung ist zweifellos die Zertifikatsverwaltung – dieser Artikel und der Abschnitt zu den Zertifikatsverwaltungsoptionen sollen dabei helfen, diese Hürde zu überwinden.
