📌

OrganizationのSHA Pinning Enforcementを有効にするまでの振り返り

に公開

はじめに

こんにちは。WebFront とかその他周辺の業務を行っている redshoga です。

最近、GitHub の Organizationの SHA Pinning Enforcement を有効にしました。

近年はサプライチェーン攻撃が増加しており、依存関係管理の重要性がこれまで以上に高まっています。直近の GitHub Actions を対象としたサプライチェーン攻撃の例としては、以下のようなものがあります。

本記事では、私が Organization 全体で SHA Pinning Enforcement を有効化するまでの取り組みの概要および振り返りをまとめています。

似たような対応はどこの組織でも必要になるはずなので、そのような方々の参考、一助になれば幸いです。

SHA Pinning の重要性

やりがちな書き方

GitHub Actions のワークフローでサードパーティアクションを利用する際、ドキュメントに記載されている例をそのままコピペして使うことが考えられます。例えば、次のような指定です。

steps:
  - uses: example-user/benri-workflow@v1
  - ...

ここで example-user/benri-workflow@v1 は、「example-user/benri-workflow リポジトリの v1 タグがついたソースコードを取得して実行する」という意味になります。

しかし、この v1 はただのタグであるため、後から中身の書き換えが可能です。

そのため、もし攻撃者が example-user/benri-workflow を変更できる権限を持つことができた場合、@v1 を参照しているすべての CI/CD が攻撃できる対象となります。(@master@main といったブランチ指定でも同様)

SHA Pinning の概要

SHA Pinning を行うと、ワークフローで参照するアクション(コード)を次のようにコミットハッシュで固定できます。

steps:
  - uses: example-user/benri-workflow@f2c1a3b7c98fd86b7e6a2e12b4c4ff35d8e902ab # v1

ここで指定されている文字列(例:f2c1a3b7...)は コミットハッシュ です。コミットハッシュを固定したままそのソースコードを変えることは現実的に不可能です。

そのため、(例えば攻撃を含んだ)意図しないバージョンのコードを実行してしまうリスクを実質的に排除できます。

Pinning されていない状態の怖さ

セキュリティの桶の理論 (セキュリティは一番弱いところから破られる考え) で考えると、とあるアクションに依存しているワークフロー環境はそのアクションの管理のセキュリティ堅牢性を考慮しないといけないことがわかります。

さらにサードパーティアクションが別のサードパーティアクションに依存しているケースもあります。その場合、依存関係が深くなるほど管理されるべき鍵は増え、セキュリティ堅牢性は指数的に低下します。

たとえば、サードパーティアクションの作者がカフェでPCを開きっぱなしにしていたら?間違ってキーを公開してしまったら?メンテナーやコントリビューターの中に悪意のあるユーザーがいたら?などと考えると Pinning されていない状況はだいぶ怖い状態であることだとわかると思います。

余談: SRI と SHA Pinning の考え方

似たような話として Web の **Subresource Integrity(SRI)**があります。

以下のように SRI の指定なしで記述した場合、攻撃などで https://cdn.example.invalid/jquery.min.js に悪意あるコードを加えられてもそのまま実行されてしまいます。

<script src="https://cdn.example.invalid/jquery.min.js"></script>

一方以下のように SRI を指定している場合、ハッシュの不一致を検知され script は実行されません。

<script
  src="https://cdn.example.invalid/jquery.min.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"></script>

SHA Pinning は、まさにこの SRI と同様の「実行コードの固定」の役割を果たします。

SHA Pinning Enforcement を有効にするまでの流れ

SHA Pinning Enforcement について

GitHub には、ワークフローで参照するアクションを SHA Pinning していない場合に実行を拒否する「SHA Pinning Enforcement」という設定項目があります。

これは リポジトリ単位Organization 単位 のどちらでも設定でき、それぞれ次のように動作します。

  • リポジトリ単位で有効化した場合

    そのリポジトリ内のすべてのアクションに対して、SHA Pinning が必須になります。

  • Organization 単位で有効化した場合

    Organization は以下のリポジトリのすべてのアクションに対して、SHA Pinning が必須になります。

※ アクションが “他のアクション” を参照している場合、その依存先のアクションについても SHA Pinning が必要になります。そのためアクション本体だけでなく、すべての依存先のアクションも SHA で固定されていないとアクション全体が動作しなくなります。

個別のリポジトリの設定画面上のSHA Pinning Enforcementの設定
個別のリポジトリの設定画面上のSHA Pinning Enforcementの設定項目 (右下の”Require actions to … “の部分)

こうした仕様を踏まえ、以下の流れで Organization の SHA Pinning Enforcement を有効化しました。

1. 対応が必要なリポジトリの洗い出し

弊社の場合 Organization 配下のリポジトリは200個以上あり、手作業は骨が折れます。

そのため Organization 配下のリポジトリから、直近 1 年以内に GitHub Actions が実行されており、かつ SHA Pinning Enforcement が有効化されていないリポジトリを抽出するスクリプトを用意しました。(LLM作)

以下を実行し、対応が必要なリポジトリを洗い出しました。

#!/usr/bin/env bash

ORG="GitHubの組織ID"
TARGET_RANGE_DAYS=365

now=$(date -u +%s)
target_range=$((TARGET_RANGE_DAYS*24*60*60))

# archivedなリポジトリを除外してリポジトリ一覧を取得
repos=$(gh repo list ${ORG} --limit 1000 --json name,archivedAt --jq '.[] | select(.archivedAt == null) | .name')

for repo in $repos; do
  # 対象のリポジトリのGitHub Actionsのログを取得
  latest_run=$(gh api "/repos/${ORG}/${repo}/actions/runs?per_page=1" -q '.workflow_runs[].created_at' --jq '.workflow_runs[].created_at')

  if [ -z "$latest_run" ]; then
    continue
  fi
  
  # 1年以内に対象のリポジトリのGitHub Actionsが使われていたら
  latest_run_timestamp=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$latest_run" +%s); 
  diff=$(( now - latest_run_timestamp ))
  if (( diff <= target_range )); then

     # 対象のリポジトリの設定上でSHA Pinが必須になっているかチェック ※
     sha_pinning_required=$(gh api "repos/${ORG}/${repo}/actions/permissions" --jq '.sha_pinning_required')
     if [ "$sha_pinning_required" = "false" ]; then
       echo "$repo"
     fi
  fi
done

repos/${ORG}/${repo}/actions/permissions の中身は権限によって取得できなかったりします。Organization の admin 権限を持っておくと、すべてのリポジトリで設定を取得できるはずです。

2. 各リポジトリのワークフローで SHA Pinning を実施する

pinact を使うと楽です。

今回はリポジトリの主なコントリビューターにお願いして対応してもらいました。(感謝)

(後述しますが、AI で PR をつくって対応する方が効率が良いかもしれないです。)

3. 個別リポジトリ上の SHA Pinning Enforcement を有効化

設定画面から有効化します。(調べたところ terraform には設定項目はありませんでした。)

念の為ワークフローが壊れていないか確認すると良いです。前述の通りワークフロー上のアクションを Pinning してもその依存先のアクションが Pinning されていない場合、そのワークフローは止まるためです。また単純につけ忘れのチェックの意味もあります。

4. Organization の SHA Pinning Enforcement を有効化する

全リポジトリの対応が完了したことを上記のスクリプトで再確認したうえで、最後に Organization レベルで Enforcement を有効化します。

振り返り

AIを活用して SHA Pin する PR をつくってもらってそれをレビューしてもらうアプローチをすればもっと効率的だったなと思っています。pinact や(PR を作成するための)ghコマンドの実行権限を渡せば適切にやってくれたはずです。

今回はリポジトリの洗い出し → 個別のリポジトリの設定の確認 → 全体の適用のように慎重に対応を行いました。実はここまで慎重にやる必要はなく、設定して止まったら解除していくとかでも良かったようにも感じています。主たるリポジトリの Actions が止まることは問題かもしれませんが、その他のリポジトリは止まったとしてもそこまで問題ではないことがあります。(セキュリティ的に危険な状態が長く続いている方が組織としてはリスク)

止まったとしても SHA Pin するか急ぎであれば Enforcement を一旦外すなどのアプローチができます。セキュリティ的に危険な状態が長く続いている方が組織としてはリスクという考えではこちらのアプローチの方が適切だったのではと考えています。

感想

GitHub Actions は現時点では強く注意しないとセキュリティ的に脆弱性を持ちやすい状態であると感じました。例えば新規リポジトリ作成時のデフォルトの設定で SHA Pinning Enforcement は有効になっていません(作成時にすぐに有効にしておくと良いです)。また GitHub Actions の Get started にも SHA Pinning は言及されていません。

CI/CD という環境上強い権限のシークレットを扱うことも多く、セキュリティとして意識すべき点は多くあるので組織としてその点に投資しちゃんと対策することが重要です。

宣伝

興味があればぜひお話ししましょう!

プロダクト採用サイトTOP
カジュアル面談申込はこちら

Discussion