コードがクラウドMacへプッシュされると、CIは通常、すぐにチェックアウト、ビルド、テストを実行します。問題は、Gitコミットに記録される作成者名とメールアドレスが編集可能なテキストにすぎないことです。誰が作成したと申告されているかは分かっても、その開発者が実際にオブジェクトへ署名したことまでは証明できません。より確実なのは、コミットにSSH署名を付け、管理された公開鍵リストを使って、マージ基準点から現在のコミットまでの全範囲をCIで検証する方法です。
このゲートはコードレビューの代わりになるものではなく、コードが安全かどうかを判定するものでもありません。対象とするのは、より限定的ですが重要な問題です。つまり、ビルドチェーンに入るGitオブジェクトが完全であるか、署名が有効であるか、そして署名鍵が現在コードの提出を許可されている人物のものかを確認します。
検証の信頼境界を先に定義する
コミットの真正性を検証するには、少なくとも次の3段階の判定が必要です。
- Gitオブジェクトの内容と署名が一致するか。
- 署名に使われた公開鍵が許可署名者リストに含まれているか。
- そのIDがコミット作成時点でもコードの提出権限を持っていたか。
第1段階は暗号署名、第2段階はリポジトリで管理するリストによって確認します。第3段階には、引き続きチームの権限剥奪プロセスが必要です。git log --show-signature を実行して「Good signature」と表示されるだけでは不十分です。有効ではあっても未承認の鍵でも、有効な署名を生成できるためです。
メールアドレスは表示と通知、公開鍵は真正性の検証、許可署名者リストは認可に使用します。この3つを同じものとして扱わないでください。
リストは、変更を厳格に管理できる独立したディレクトリ、たとえば .ci/trusted_signers に配置することを推奨します。リスト自体の変更には追加レビューを必須とし、提出者が同じ変更の中で自分の公開鍵を追加し、そのままコードを通過させられないようにします。
GitのSSHコミット署名を設定する
Git 2.34以降では、SSH鍵を使って直接署名できます。まず開発者のローカル環境で、署名形式と公開鍵のパスを設定します。
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgSign true
ここで指定するのは公開鍵ファイルであり、実際の署名には対応する秘密鍵が使われます。コミットを作成したら、オブジェクトに署名が実際に含まれていることを確認します。
git cat-file commit HEAD | sed -n '/^gpgsig /,/^[^ ]/p'
許可署名者ファイルの各行には、ID、任意の制約、公開鍵を記載します。IDには、頻繁に変わる表示名ではなく、安定したチーム識別子を使用します。
ci-release namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
ios-team namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5BBBB...
CIがリポジトリをチェックアウトしたら、このファイルをGitに指定します。
git config --local gpg.format ssh
git config --local gpg.ssh.allowedSignersFile .ci/trusted_signers
git verify-commit HEAD
秘密鍵をリポジトリへ保存してはいけません。CIの検証処理に必要なのは公開鍵だけです。秘密鍵が必要なのは、署名付きコミットまたはタグを作成する環境に限られます。
コミット範囲全体にゲートを適用する
HEAD だけを検証するのは、よくある誤りです。攻撃目的または未承認の変更を1つ前のコミットに隠し、最後の署名付きコミットで表面上の状態を上書きすることができます。ゲートでは、信頼できる基準点より後にある全コミットを走査する必要があります。
#!/bin/bash
set -euo pipefail
base="${MERGE_BASE_SHA:?missing MERGE_BASE_SHA}"
head="${HEAD_SHA:?missing HEAD_SHA}"
git cat-file -e "${base}^{commit}"
git cat-file -e "${head}^{commit}"
count=0
while IFS= read -r commit; do
git verify-commit "$commit"
count=$((count + 1))
done < <(git rev-list --reverse "${base}..${head}")
printf 'verified_commits=%s\n' "$count"
MERGE_BASE_SHA は、CIが対象ブランチとマージ対象ブランチから計算するか、信頼できる方法で渡す必要があります。単純に HEAD~1 を指定してはいけません。マージコミットでは、基準点の選択がチームのポリシーに合っていることも確認してください。そうしないと、第2親ブランチから取り込まれたオブジェクトを見落とす可能性があります。
ゲートの出力にはコミットハッシュと失敗した段階を記録しますが、コミットメッセージ全体や環境変数を公開ログへコピーしてはいけません。失敗時にビルドを停止する方が、出所を確認できない成果物をそのまま生成するよりも監査が容易です。
浅いクローン、タグ、鍵更新に対応する
浅いクローンは、スクリプトによる誤検知の原因になりがちです。基準点のオブジェクトがローカルに存在しなければ、rev-list で完全な範囲を取得できません。検証前に対象ブランチの必要な履歴を追加取得し、git cat-file -e で基準点の存在を明示的に確認します。基準点が見つからない場合に、HEAD だけの検証へ縮退させてはいけません。
| シナリオ | 正しい対処 | 避けるべき近道 |
|---|---|---|
| 浅いクローンに基準点がない | 必要な履歴を取得してから範囲を再計算する | 最後のコミットだけを検証する |
| リリースタグ | 署名付き注釈タグを使用し、git verify-tag を実行する |
タグ名だけを確認する |
| 新旧の鍵を切り替える | 短期間は両方の公開鍵を保持する | すぐに置き換えて過去のジョブを失敗させる |
| 鍵が漏えいした | 直ちに削除し、その鍵が署名した範囲を再確認する | 表示上のIDだけを変更する |
| メンバーが離脱した | その人物の許可署名者レコードを削除する | 通常のログイン権限だけを無効にする |
鍵の削除後も過去のコミットを合格させるかどうかは、事前に決めておく必要があります。最も単純な厳格モードは「現在のリストで検証する」方式で、マージゲートに適しています。過去のリリースを長期にわたって検証する必要がある場合は、有効期間を含む信頼記録を保存し、リリースタグ、コミットハッシュ、当時使用したリストのバージョンをまとめてアーカイブします。
導入前チェックとトラブルシューティング
正式にマージをブロックする前に、まず観察期間を設け、失敗を記録しつつ、未署名のリリースは通過させない運用ができます。観察期間中は、特に次の項目を確認します。
- 開発者が使用するGitのバージョンがSSH署名をサポートしている。
- 通常のコミット、マージコミット、自動生成コミットのすべてで署名者を明確に特定できる。
- CIが固定の深さに依存せず、基準点までの完全な履歴を取得している。
- 許可署名者ファイルの変更に独立したレビューが必要になっている。
- リリースタグと、そのタグが指すコミットを個別に検証している。
- BotのIDには専用鍵を使用し、個人と共有していない。
- 鍵更新、鍵の漏えい、メンバー離脱について実行可能な手順が用意されている。
失敗を調査するときは、まず git verify-commit --raw <hash> を使い、「オブジェクトに署名がない」「署名が破損している」「公開鍵が許可リストにない」を切り分けます。続いて、リポジトリ単位の gpg.format と gpg.ssh.allowedSignersFile の設定、およびリスト内の名前空間、鍵タイプ、改行を確認します。これにより、同じコミットへの再署名を繰り返すのではなく、IDの認可問題とGit履歴の不足を切り分けられます。
コミット単位の検証、タグの検証、リスト変更のレビューがすべてパイプラインに組み込まれると、ビルドシステムは監査可能な出所の連鎖を持つようになります。コードに欠陥がないことまでは証明できませんが、どのオブジェクトがどの許可済み鍵で署名されたか、また未確認のオブジェクトがなぜ後続のビルドへ進まなかったかを明確に説明できます。
よくある質問
作者メールが正しければコミットを信頼できますか?
できません。名前とメールはコミット作成者が自由に設定できる文字列です。署名で秘密鍵の保有を確認し、許可署名者リストでその鍵が承認済みの本人に属するかを判断します。
ブランチ先端のコミットだけを検証すれば十分ですか?
不十分です。信頼する基点より後の全コミットが最終ソースに影響するため、範囲内の各コミットにgit verify-commitを実行します。リリースタグにはgit verify-tagも必要です。
CIを止めずに署名鍵を更新するにはどうしますか?
新しい公開鍵をバージョン管理された許可リストへ先に追加し、移行中は新旧両方を許可します。新しい鍵の検証後に古い鍵を削除します。
次の開発タスク用にクラウドMacを構成
Oak M4、Oak M4 Plus、Oak M4 Proから選び、チームの所在地に合わせて5つの物理ノードからレンタル構成を設定できます。