エンジニアリング記事

クラウドMac CIのGit認証情報を隔離して確実に消去する

クラウドMac CIのGit認証情報を隔離して確実に消去する

長時間稼働するクラウドMacで複数リポジトリのビルドを連続実行する場合、最も危険なのは、必ずしもトークンがスクリプトに直接書かれていることではありません。前のジョブが認証情報を残し、次のジョブがそれを暗黙のうちに引き継いでしまうことも大きなリスクです。代表的な残留箇所には、Gitのグローバル設定、認証情報を含むリモートURL、ログインキーチェーン、削除されていない一時スクリプトがあります。この問題への対策は「ビルド終了後に変数を1つ削除する」ことではなく、最初から認証情報を単一ジョブの境界内に限定することです。

認証情報の保存先を先に確認する

すぐにパイプラインを変更するのではなく、まず秘密の値を出力せずに、Gitが実際にどの設定を読み込んでいるかを確認します。--show-originを使うと設定元も同時に表示されるため、システム、ユーザー、リポジトリの各レベルを区別できます。

set +x

git config --show-origin --get-all credential.helper || true
git config --show-origin --get-regexp '^(credential|http)\.' || true
git config --show-origin --get-regexp '^url\..*\.insteadof$' || true

特に確認すべきシグナルは次の3つです。

  • credential.helperが永続的なキーチェーンを参照していないか。
  • http.extraHeaderがグローバル設定またはリポジトリ設定に書き込まれていないか。
  • url.*.insteadOfによって通常のURLが認証情報を含むURLへ書き換えられていないか。

リモートURLも確認する必要がありますが、そのままログへ出力してはいけません。スキーム://ユーザー情報@ホストという構造が存在するかだけを判定し、見つかった場合は完全なURLを表示せず、直ちに失敗させます。

remote_url="$(git remote get-url origin)"
case "$remote_url" in
  *://*@*)
    printf '%s
' "Remote URL contains embedded credentials" >&2
    exit 1
    ;;
esac

監査スクリプトの第一原則は、「認証情報が存在する可能性」だけを報告することです。問題を証明するために認証情報そのものをビルドログへ出力してはいけません。

ジョブごとに独立したHOMEを作成する

GitはHOMEを基準にユーザーレベルの設定パスを決定します。すべてのジョブで実行ユーザーのHOMEを共有すると、.gitconfig、credential helperの設定、多数のツール状態まで共有することになります。より安全なのは、ジョブごとにパーミッション700の一時HOMEを作成し、Gitのグローバル設定ファイルを明示的に指定する方法です。

set -eu

ORIGINAL_HOME="$HOME"
JOB_HOME="$(mktemp -d "${TMPDIR%/}/git-job.XXXXXX")"
chmod 700 "$JOB_HOME"

export HOME="$JOB_HOME"
export XDG_CONFIG_HOME="$JOB_HOME/.config"
export GIT_CONFIG_GLOBAL="$JOB_HOME/.gitconfig"
export GIT_TERMINAL_PROMPT=0

mkdir -p "$XDG_CONFIG_HOME"

この方法ではシステムレベルのGit設定までは隔離されないため、helperチェーンを明示的にリセットする必要があります。Gitでは複数のcredential.helper値を設定できます。最初に空の値を書き込むことで、優先順位の低い設定から継承されたhelperを消去し、その後に今回のジョブ専用の実装を追加できます。

リポジトリまで一時HOMEに置かない

HOMEは認証情報とツール状態の境界であり、ワークスペースの役割まで兼ねる必要はありません。ソースコードのディレクトリは引き続きrunnerのワークスペースで管理すると、容量管理や成果物の収集が容易になります。両者を分離すれば、HOMEのクリーンアップでビルド結果を誤って削除することも、ワークスペースのクリーンアップでユーザーレベルの認証設定を取りこぼすこともありません。

パイプラインを並列実行する場合は、並列スロットごとにmktempを1回呼び出す必要があり、リポジトリ名を基準に固定ディレクトリを再利用してはいけません。固定ディレクトリは異常終了後に次のジョブへ引き継がれるうえ、2つのジョブが同じ.gitconfigを同時に変更する可能性もあります。

Gitから要求されたときだけトークンを渡す

トークンはCIのシークレット変数機能を通じて環境へ注入し、スクリプトでは変数名だけを参照します。clone URLにトークンを埋め込んだり、git config --global http.extraHeaderで認証ヘッダーを永続的に保存したりしてはいけません。

次のhelperはGitのget要求にだけ応答します。設定ファイルに保存されるのは変数を参照するロジックであり、トークンの値ではありません。

: "${CI_GIT_USER:?CI_GIT_USER is required}"
: "${CI_GIT_TOKEN:?CI_GIT_TOKEN is required}"

git config --global credential.helper ""
git config --global --add credential.helper \
'!f() {
  if [ "$1" = get ]; then
    printf "username=%s
password=%s
" \
      "$CI_GIT_USER" "$CI_GIT_TOKEN"
  fi
}; f'

コマンドエコーと対話的フォールバックを無効にする

シークレット変数を扱う前に、必ずset +xを実行します。そうしないと、shellが変数展開後のコマンドをログへ書き出してしまいます。GIT_TERMINAL_PROMPT=0も同様に重要です。トークンが存在しない場合や無効な場合、ジョブは明確に失敗すべきであり、画面に見えない対話プロンプトで停止してはいけません。

ビルドラッパーが環境変数を自動的にダンプしないかも確認してください。診断情報では、文字列の長さが0より大きいかを確認するなど、変数が設定されているかどうかだけを出力します。変数の内容、認証ヘッダー、完全なリモートURLを出力してはいけません。

書き込み権限が必要な自動化タスクでは、ソースコードの読み取りと成果物のpushに別々の認証情報を使用するべきです。checkoutしか行わないジョブに書き込み権限は不要です。リリースジョブにも、対象リポジトリと必要な操作範囲を超える権限を与えてはいけません。

trapですべての終了経路を処理する

スクリプトの最後にrm -rfを書くだけでは、途中の失敗、タイムアウトによる終了、手動キャンセルに対応できません。一時ディレクトリを作成した直後にtrapを登録し、クリーンアップ関数では最初に変数を消去してからジョブのHOMEを削除します。

cleanup_git_credentials() {
  set +e
  unset CI_GIT_TOKEN CI_GIT_USER
  if [ -n "${JOB_HOME:-}" ] && [ -d "$JOB_HOME" ]; then
    rm -rf "$JOB_HOME"
  fi
}

trap cleanup_git_credentials EXIT HUP INT TERM

クリーンアップ関数は繰り返し実行できなければならず、ディレクトリがすでに存在しなくてもエラーにしてはいけません。削除対象についても、変数が空ではないことと、実際にディレクトリを指していることの2条件を満たす必要があります。広範囲を対象とするキーチェーン削除コマンドを使用したり、ログインキーチェーン全体を消去したりしてはいけません。同じ実行ユーザーが必要とする別の項目が保存されている可能性があるためです。

過去のパイプラインで永続的なhelperを使用していた場合は、まずホストとアカウントを基準に正確な棚卸しを行い、その後で一度限りの移行を計画します。新旧の方式を並行運用している間は、ジョブごとに現在のhelperの参照元を検証してください。一時HOMEを有効にしていても、システムのラッパースクリプトが古い設定を再びコピーする可能性があります。

残留チェックをビルドのゲートにする

クリーンアップ処理がエラーを出さなかっただけでは、成功したとは判断できません。runnerの外側で終了後チェックを追加するか、executorがジョブを回収するときに、次の項目を検証することを推奨します。

  • 一時HOMEが削除されている。
  • ワークスペースの.git/configに、認証ヘッダーやユーザー情報を含むリモートURLが存在しない。
  • ジョブログにシークレット変数の既知のフィンガープリントが含まれていない。
  • Git設定の参照元が、想定したシステム設定と今回の一時設定だけである。
  • 後続の空のジョブが、前のジョブのリポジトリ認証情報を読み取れない。

テスト用トークンには権限を持たない固定のマーカー値を設定し、隔離環境で失敗ケースを実行したうえで、ログとファイルシステムをスキャンできます。ここで確認するのは「マーカーが漏えいしたか」であり、本物のトークンを検証することではありません。失敗ケースには少なくとも、cloneの失敗、ビルドコマンドの終了、終了シグナルの受信、クリーンアップ関数の繰り返し実行を含めます。

複数のチームが1台の物理ノードを共有する場合は、実行ユーザーも第2の境界として扱う必要があります。一時HOMEが解決するのはジョブ単位の残留です。独立したユーザーは、異なる信頼ドメイン間のプロセス、ファイル権限、キーチェーンの境界を分離します。両方の層を併用して初めて、スクリプトによるクリーンアップを唯一の防御線にせずに済みます。

最終的な受け入れ基準は単純です。ジョブ開始前に継承可能な認証情報がなく、実行中は必要なときだけ提供され、どの終了経路でもクリーンアップされ、次のジョブからは前のトークンが存在していたことすら確認できない状態です。ここまで実現して初めて、Git認証情報は長時間稼働するクラウドMacではなく、個々のジョブに属するものになります。

よくある質問

ジョブ終了時にトークン変数をunsetするだけでは不十分ですか?

不十分です。helper、リモートURL、グローバル設定、キーチェーンに保存済みの可能性があるため、それぞれを個別に確認する必要があります。

常駐するMac CIでログインキーチェーンを共有してもよいですか?

異なるリポジトリや信頼境界では共有しない方が安全です。短命かつ最小権限のトークンを使い、必要なら実行ユーザーやキーチェーンを分離します。

専用クラウドMac

開発、ビルド、リモートデスクトップに専用物理マシンを選択

3種類のApple Silicon構成を比較し、注文時にノード、利用期間、ストレージの追加オプションを選択できます。

レンタルプランを選ぶ