macOS コード署名の受け渡しをプロダクトチームと IT/SRE チームで分担する — Account Holder 作業と GitHub Environment Secrets 登録を分ける
macOS デスクトップアプリを Mac App Store の外で本番配布するとき、Developer ID Application 証明書での署名と、notarytool による公証を済ませないと Gatekeeper に受け入れてもらえません。これは Electron・Tauri・Flutter desktop・native Swift/AppKit など、どのフレームワークで作ったアプリでも共通で、どこで署名・公証を走らせるかを決める必要があります。GitHub Actions の macOS ランナーで署名・公証まで完結させると、キーチェーンを使った手作業を CI から外せる一方で、法人の Apple Developer Program を保有しているチームと、プロダクトの repo を運用しているチームが別の組織構造だと、「誰がどのタイミングで何を触るか」の分担が必要になります。
本記事では、法人の Apple Developer Program を IT/SRE チームが保有し、プロダクト repo の GitHub Actions ワークフローをプロダクトチームが運用する体制を想定して、macOS コード署名の受け渡し手順を 2 チーム間でどう分担するかを書き留めます。
対象シナリオ: 法人の Apple Developer Program と、プロダクトの CI を別チームが運用する
今回想定するのは次の構造です。
- IT/SRE チームが法人としての Apple Developer Program を契約し、App Store Connect 上の Account Holder を保有する
- プロダクトチームが、macOS デスクトップアプリの GitHub repo を所有し、GitHub Actions ワークフローで electron-builder・Tauri CLI・xcodebuild などの手段でビルド・署名・公証・staple を回す
- 両チームとも GitHub organization の同じ repo を見るが、Secret の登録や Environment の設定は repo の Admin 権限が必要
- 法人共通の Apple ID やキーチェーンは IT/SRE 側の MDM 配下の Mac にあり、プロダクトチーム側の端末には秘密鍵を置かない前提にする
この構造は、事業会社と開発会社で分担するケースに近いものですが、同じ会社の中でも「情シス・セキュリティ・SRE をまとめた IT/SRE チーム」と「プロダクトを運営するチーム」が分かれている組織では珍しくありません。どちらも、Apple の仕様で Account Holder のロールが必要な作業と、GitHub の仕様で repo Admin にしか行えない作業の両方を踏まないと最後まで通らない、という点では共通です。
受け渡す値を 4 種類に分けて、秘匿区分を揃える
分担の設計をする前に、CI まで通すために実際に受け渡される値を分類しておきます。
| 種別 | 内容 | 用途 | 秘匿区分 |
|---|---|---|---|
| A | Team ID(10 桁の英数字) | 署名者の識別。CI の APPLE_TEAM_ID | 公開情報(署名済みアプリに埋め込まれる) |
| B | Developer ID Application 証明書の .p12(秘密鍵込み)と書き出しパスワード | アプリ本体への署名。CI の DESKTOP_MAC_CSC_LINK・DESKTOP_MAC_CSC_KEY_PASSWORD | 秘密 |
| C | App Store Connect API Key(AuthKey_XXXXXXXXXX.p8)・Key ID・Issuer ID | Apple の公証サービスへの提出。CI の APPLE_API_KEY・APPLE_API_KEY_ID・APPLE_API_ISSUER | 秘密(Issuer ID は準公開) |
| D | 管理台帳情報(証明書の名義、Team ID、有効期限、Key ID、担当者) | 更新・失効時の連絡先、監査対応 | 公開可 |
A と D は Issue のコメントや repo の README に書いて構わない情報で、B と C は秘密鍵相当です。秘匿区分を揃えておく利点は、「どのチームが・どの保管先に・どのロールで値を触るか」が区分ごとに決まることです。公開情報は Issue 本体や GitHub Actions のログに残ってよく、秘密情報は後述の案 A・案 B の経路に限る、という切り分けができます。
Account Holder のロールが必要な作業は IT/SRE 側に閉じる
Apple の公式手順に明記されているとおり、Developer ID Application 証明書の発行に必要なロールは Account Holder です。法人契約の Apple Developer Program では、Account Holder は IT/SRE 側の管理者または経営層が担っていることが多く、プロダクトチームのメンバーが Admin ロールを持っていても、本記事で扱う手元の Mac で CSR を作る経路では証明書を作れません。Apple には cloud-managed 証明書という別の仕組みもありますが、本記事は .p12 を CI に渡す構成を前提にするため扱いません。
このため、IT/SRE 側で完結させる作業を次の順序で揃えました。
- Membership details から Team ID を控える
- キーチェーンアクセスの「証明書アシスタント」で CSR を作る(鍵ペア情報を指定し、RSA 2048 ビット)
- Certificates, Identifiers & Profiles で Developer ID Application を選び、
.cerをダウンロードする(以前の Sub-CA で発行した証明書の置き換えは Apple の手順を参照) .cerを同じ Mac のログインキーチェーンに取り込み、security find-identity -v -p codesigningでDeveloper ID Application: <法人名> (<Team ID>)が返ることを確認する- 証明書を右クリックして「書き出す…」から
.p12を作り、長めのランダム文字列を書き出しパスワードに設定する
秘密鍵は CSR を作った Mac のキーチェーンに生成されるため、CSR 作成から .p12 の書き出しまでは同じ Mac の中で完結します。この端末を IT/SRE 側の MDM 配下 Mac に固定すると、秘密鍵が存在する端末を IT/SRE 側の 1 台に限定でき、プロダクトチーム側の端末には秘密鍵を一度も置かずに済みます。
一方で、Account Holder が実際の作業者と別人の場合は、.certSigningRequest ファイルを Account Holder に渡し、発行された .cer を受け取る形でも成立します。発行作業そのもの(Developer ID を選ぶ画面と、Download ボタン)は、この経路では Account Holder が操作します。
公証 API Key は Team Key で、組織境界ごとに分けて発行する
公証は Apple ID + パスワードではなく、App Store Connect API Key 方式で行います。CI で 2 要素認証を扱わずに済ませるためで、notarytool が受け付けるキーの種類は限られます。
- 「個人キー(Individual Key)」は notarytool で使えないため、「チームキー(Team Keys)」側から発行する
- アクセスロールは
Developerを選ぶ(公証に必要十分な最小ロール) - 名前は後述の組織境界が識別できるものにする(例:
inhouse-notarization・vendor-foo-notarization) - 生成直後に 1 度だけダウンロードできる
AuthKey_<Key ID>.p8を保存する(再取得不可) - Issuer ID と Key ID を記録する
分けるときの単位は、プロダクト数ではなく後述の証明書と同じ組織境界に揃えます。自社だけで完結する場合は 1 本、開発会社ごとに分かれる場合はその単位で 1 本ずつ発行し、既存の key を流用しないルールにします。流用すると、漏えいや失効の際に別の組織境界の CI まで一斉に止まるためです。Key ID 単位で Apple 側のログに利用元が残り、境界ごとに独立して失効できるようにしておきます。
Developer ロールだけを割り当て、Admin や Developer Resources ロールを付けないのも、公証以外の API 操作を遠ざけるためです。
Secret 登録は IT/SRE が GitHub Environment に直接書き込む経路を優先する
B(.p12 と書き出しパスワード)と C(.p8・Key ID・Issuer ID)を、プロダクト repo の GitHub Environment Secrets にどう反映するかで、2 つの経路が取れます。
- 案 A(推奨): IT/SRE 側が GitHub Environment Secrets を直接登録する。秘密鍵と API Key の実体が IT/SRE チームの外へ一度も出ない
- 案 B: 秘密鍵と API Key を 1Password 共有 Vault などの安全な経路でプロダクトチームへ受け渡し、プロダクトチーム側が GitHub へ登録する
案 A では、プロダクトチームが IT/SRE の登録担当者を一時的に repo の Admin ロールへ昇格させ、登録が終わったら元の権限に戻します。Environment Secret の登録には repo の Admin 権限が必要で、Write 権限では Settings › Environments 画面に書き込めないため、短時間の権限貸与が発生します。この間 IT/SRE 側は repo の内容すべてに Admin としてアクセスできる状態になるので、プロダクトチーム側で事前に機微な情報が残っていないか棚卸しておくと安全です。
Environment の development と production の両方に、次の 6 件を登録します。
| Secret 名 | 値 |
|---|---|
APPLE_TEAM_ID | A の Team ID |
DESKTOP_MAC_CSC_LINK | .p12 を改行なし base64 にした文字列(base64 -i xxx.p12 | tr -d '\n' | pbcopy) |
DESKTOP_MAC_CSC_KEY_PASSWORD | .p12 書き出し時のパスワード |
APPLE_API_KEY | AuthKey_<Key ID>.p8 のファイル内容すべて(PEM の BEGIN から END まで) |
APPLE_API_KEY_ID | C の Key ID |
APPLE_API_ISSUER | C の Issuer ID |
Secret 名は repo ごとの命名で、署名ツールが読む環境変数名とは一致しません。たとえば electron-builder には、workflow で DESKTOP_MAC_CSC_LINK を CSC_LINK に、DESKTOP_MAC_CSC_KEY_PASSWORD を CSC_KEY_PASSWORD に対応させて渡します。
案 B は、IT/SRE 側で GitHub 操作を行う権限や時間を取りづらいケースに取る経路で、1Password 共有 Vault の期限付きアクセスか、ITIL 承認済みのファイル共有サービスの期限付きリンク(パスワードは別経路)で .p12 と .p8 を受け渡します。この場合は、プロダクトチーム側に秘密鍵の実体が渡るため、プロダクトチーム側のメンバーがどの程度 Secret 衛生のトレーニングを受けているか、ダウンロード端末が MDM 配下か、ダウンロードフォルダから速やかに退避できるかなどの前提が増えます。
どちらを選ぶかは、「プロダクトチーム側に秘密鍵を安全に置ける端末があるか」「IT/SRE 側で Admin ロールの時限貸与が運用できるか」で決めます。判断材料は権限と端末管理の運用可否であり、チーム間の信頼ではありません。両方の運用が整っている場合は案 A を選びます。秘密鍵が IT/SRE チームの外へ出ないため、受け渡し経路が 1 本減り、監査で説明する範囲も狭くなります。
プロダクトチーム側の責務: workflow 配線と Environment の分離、検証
Secret が揃った後は、プロダクトチームの手番です。CI 側で証明書と API Key を実際に使うのはプロダクトチームのビルド・配布ワークフローなので、Environment の分離と検証はこちら側で持ちます。
developmentEnvironment を Protection Rules なしで「ビルド専用(配布なし)」実行できるようにし、.appと.dmgの署名・公証・staple が通ることを検証するproductionEnvironment に Required reviewers を付け、本番配布は別途承認を経てからしか走らないようにする- 検証ジョブで
spctl --assess --verbose=4がaccepted / Notarized Developer IDを返し、codesign -dv --verbose=4のAuthorityが IT/SRE 側の法人名義になることを確認する developmentの実行で問題がなければ、production側の本番配布経路を続けて検証する
署名ツールが参照する環境変数名(electron-builder なら CSC_LINK・CSC_KEY_PASSWORD・APPLE_API_KEY・APPLE_API_KEY_ID・APPLE_API_ISSUER、Tauri CLI なら APPLE_CERTIFICATE・APPLE_CERTIFICATE_PASSWORD、xcrun notarytool なら --key・--key-id・--issuer 相当)に、GitHub Secret から値を流し込む配線は repo 側の yaml に書き、Secret の中身は IT/SRE 側に閉じる、という切り分けになります。workflow 側は Secret 名しか知らず、GitHub Actions のログでは Secret の値がマスクされます。
証明書は組織境界で分けて 5 枚の枠を活かす
Developer ID Application 証明書は、1 Team あたり最大 5 枚まで発行できます。この枠をどこで切るかは、プロダクト数ではなく「秘密鍵の管理責任が別になる組織境界」で切ると、失効や漏えいの影響範囲が境界の内側に収まります。
- すべてを自社内で署名・配布する場合は、1 枚で複数のプロダクトを署名して構わない。秘密鍵の管理責任は IT/SRE チーム 1 箇所に集約されていて、分けても得るものが少ない
- 事業会社が複数の開発会社に委託している場合は、開発会社ごとに 1 枚発行する。失効・漏えいが起きたとき、他社の CI まで巻き込まない
- 子会社・事業部など、法務上または CI 運用上の責任が分かれているグループが複数ある場合も、その単位で 1 枚を割り当てる
- プロダクトのライフサイクルが大きく違い、片方の失効がもう片方の署名・公証を止めないようにしたい場合は、別途 1 枚割り当てる
5 枚の枠は、プロダクト数ではなく「秘密鍵を独立して管理したい相手の数」に対して配分すると、プロダクトが増えても 5 枚の上限に達しません。たとえば自社 + 開発会社 3 社構成なら、自社 1 枚 + 開発会社 3 枚で合計 4 枚に収まり、1 枚余ります。プロダクトが 10 個あっても、それらが同じ 4 つの境界のどこかに属していれば、枠は 4 枚で足ります。
同じ組織境界で 1 枚の証明書を複数プロダクトが共有する場合も、本記事の案 A(IT/SRE 側が GitHub Environment Secret を直接登録する経路)がそのまま使えます。共有先の各 repo の Environment に秘密鍵と書き出しパスワードを登録するだけで、プロダクトチーム側に秘密鍵を渡さずに回せます。
5 枚の枠に収まらないほど境界の数が多い場合は、別法人・別事業部で 別の Apple Developer Program を契約し、Team ID を分けて 5 枚の枠を増やす選択肢もあります。契約・年会費・管理台帳が二重化するコストと、分けたい相手の数を見比べて判断します。
なお、Apple Distribution 証明書(App Store 配布用)とは別種類なので、発行枠は共有せず、Developer ID と App Store 配布を同時に進めていても衝突しません。
管理台帳と更新サイクルは双方で共有する
管理台帳 D は、どちらか片方のチームだけが持つのではなく、双方で参照できる形にしておきます。更新・失効のタイミングで両方のチームが動く必要があるからです。
台帳に残す項目は次を基本にしています。
- 証明書の名義(法人名)と Team ID
- 証明書の有効期限(Certificates 画面に表示される日付)
.p12の指紋(SHA-256)と保管場所(1Password アイテムの参照 URL)- Key ID、Issuer ID、API Key の名前と作成日
- IT/SRE 側とプロダクトチーム側の管理・更新担当者
- Apple Developer Program メンバーシップの年次更新日
更新時のオペレーションは、「新しい値を発行 → GitHub の値を差し替え → 検証 → 旧値を失効」の順で、どちらかを飛ばさないようにします。更新の実作業は IT/SRE 側で、GitHub Environment Secret の差し替えは案 A・案 B の経路に合わせ、台帳の更新は双方で行う分担にします。Apple Developer Program の年会費によるメンバーシップ期限切れには、専用のアラートを設けます(期限切れでも配布済みアプリは動きますが、新しい証明書は発行できなくなります)。
分担のフロー図
ここまでの分担をまとめると次のようになります。IT/SRE 側に閉じる作業と、プロダクトチーム側に閉じる作業を、レーンを分けて描いています。
flowchart TB
subgraph IT["IT/SRE チーム"]
direction TB
s1["Team ID を控える"]
s2["Mac で CSR を作成"]
s3["Developer ID Application 証明書を発行<br/>(Account Holder のロールが必要)"]
s4["Mac に取り込み、確認"]
s5[".p12 として書き出し(パスワード付き)"]
s6["App Store Connect API Key を Team Keys から発行<br/>(Developer ロール)"]
s7{"Secret 登録経路"}
s7a["案 A(推奨)<br/>IT/SRE が GitHub Environment Secret に直接登録<br/>秘密鍵が IT/SRE の外へ出ない"]
s7b["案 B<br/>1Password 共有 Vault でプロダクトチームへ受け渡し"]
s1 --> s2 --> s3 --> s4 --> s5 --> s6 --> s7
s7 -->|推奨| s7a
s7 --> s7b
end
subgraph PD["プロダクトチーム"]
direction TB
s8["案 A の場合: 登録担当者を一時 Admin にして登録完了後に元の権限へ戻す<br/>案 B の場合: 受領値を Environment に登録"]
s9["ビルドワークフローを development でビルド専用実行<br/>署名・公証・staple を spctl / codesign で検証"]
s10["production で本番配布経路を検証<br/>Required reviewers による承認を経て配布"]
s8 --> s9 --> s10
end
subgraph JOINT["双方"]
direction TB
s11["管理台帳に記録<br/>名義・Team ID・有効期限・Key ID・担当者"]
end
s7a --> s9
s7b --> s8
s10 --> s11
以上、法人の Apple Developer Program をもつ IT/SRE チームと、プロダクト repo の CI を運用するプロダクトチームで macOS コード署名の受け渡しを分担する設計について、現場からお送りしました。
参考情報
- Apple: Create Developer ID certificates
- Apple: Create a certificate signing request
- Apple: Locate your Team ID
- Apple: Program roles
- Apple: Creating API Keys for App Store Connect API
- Apple: Customizing the notarization workflow
- electron-builder: macOS
- Tauri: Signing macOS applications
- Apple: Packaging Mac software for distribution
- Apple Support: Gatekeeper and runtime protection in macOS
- GitHub Docs: Managing environments for deployment