GitHubでサプライチェーン攻撃を防ぐ設定 – Plan 9とGo言語のブログ

ここ数ヶ月でサプライチェーン攻撃に関連していくつかベストプラクティスが出ていたので、GitHubリポジトリに適用しておいたほうがいいものをまとめた。

被害を受けないために

Dependabotにcooldownを設定する

過去のサプライチェーン攻撃では、ほとんどは問題のあるリリースが公開されてから数時間で発見されているので、自分のリポジトリが汚染されないためにリリースから一定期間はアップデートを保留するという手段が取られるようになったと記憶している。もともとRenovateには minimumReleaseAge オプションがあったのだが、Dependabotでも cooldown オプションが使えるので設定する。

このオプションを設定しても、上の記事中に

Key benefits

  • Stay responsive to critical security patches.

とあるように脆弱性の修正は cooldown の設定よりも優先してアップデート対象となるらしいので、ある程度日数を開けても問題ないと思う。設定例は以下の通り。

version: 2
updates:
- package-ecosystem: github-actions
  directory: /
  schedule:
    interval: weekly
  cooldown:
    default-days: 5

上記では、サプライチェーン攻撃に関していえばパッチバージョンだから安全という訳でもないので default-days を設定しているが、この他にもマイナーバージョンやパッチバージョンごとに期間を設定できる。

Actionsのバージョンをハッシュで強制する

GitHub ActionsではGitのタグを使ってバージョンを特定しているので、もともと安全だったタグが攻撃者によって悪意のあるコミットに向けられてしまって汚染されることがある。こういった攻撃を防ぐため、GitHubは公式にSecure use reference#Using third-party actions

You can help mitigate this risk by following these good practices:

  • Pin actions to a full-length commit SHA

のようにハッシュによるバージョン参照を推奨していたのだが、2025年8月にハッシュで参照することを強制する Require actions to be pinned to a full-length commit SHA オプションが追加された。

公式がハッシュによる参照を推奨しているので上記のオプションも有効にするといいと思うけれど、このオプションはワークフローが依存するすべてのアクションでハッシュ指定を強制するので、外部のアクションにタグ指定がひとつでもあるとエラーになる。例えば筆者は一部のワークフローでgovulncheck-actionを使っているのだけど、2025年10月時点では govulncheck-action にはタグを使ってバージョンを参照するコードが含まれているので、上記オプションを有効にするとエラーとなってワークフローを実行できないので状況を確認しつつ有効にしていくといい。

被害を広げないために

CodeQLで検査を行う

CodeQLはGitHubが公式に提供するコードスキャナで、2025年10月時点ではCodeQLの設定は以下2通りの方法がある。

CodeQLはリポジトリの設定画面から Advanced Security タブの Code Scanning セクションで有効にできる。2025年10月時点では、上記で Require actions to be pinned to a full-length commit SHA オプションを設定していると

Code scanning with GitHub Actions is not available for this repository

と警告が出てセットアップを行えない状態となるが、その状態でも自分でワークフローを記述すれば高度なセットアップとしてCodeQLが動作する。高度なセットアップでは以下のようなGitHub Actionsのワークフローとして記述する。



name: "CodeQL Advanced"
on:
  push:
    branches:
    - main
  pull_request:
    branches:
    - main
  schedule:
  - cron: '17 2 * * 6'
jobs:
  analyze:
    name: Analyze (${{ matrix.language }})
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      packages: read
      actions: read
      contents: read
    strategy:
      fail-fast: false
      matrix:
        include:
        - language: actions
          build-mode: none
        - language: go
          build-mode: autobuild
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v3
      with:
        languages: ${{ matrix.language }}
        build-mode: ${{ matrix.build-mode }}
    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v3
      with:
        category: "/language:${{ matrix.language }}"

ワークフローとして記述する内容については以下のドキュメントにまとまっている。

コード署名を必須にする

名前とアイコンだけでは不安があるのでコード署名も有効にできればより良いが、これは過去に記事を書いた。

blog.lufia.org

Immutable Releaseを設定する

いちどGitHub Releasesで公開したリリースを変更できなくするオプションも追加された。

これを有効にした状態でリリースを作成すると、リリースした内容を変更できなくなる

このオプションを使うには、リポジトリの設定で General タブにある Release セクションで Enable Release Immutability を有効にする。Immutable Releasesを有効にした状態でリリースを作成すると、該当するリリースには鍵付きのアイコンに Immutable というラベルが付与される。

鍵アイコンにImmutableと文字が並んでいます

ただし不変となるのはコミットの内容とリリース成果物だけで、リリースのタイトル、リリースの説明文、プレリリース状態は依然として変更可能となっている。

Immutable Releasesでアセットを追加する

上記の通り、Immutable Releasesが有効になっていると、リリースにファイルのアップロードが行えなくなる。成果物をリリースに含めたい場合にどうするのかといえば、バージョンをいちどドラフトで作成して、成果物のアップロードが完了してから正式リリースに変更する手順を踏む必要がある。GitHub Releasesで各バージョンの状態は

  • Draft
  • Pre-release
  • Release

の3つで推移するので、手動でリリース成果物を追加した後で Pre-release または Release に変更すればいい。

手動またはGitHub CLI等で人間がアップロードする場合はそれでいいのだけれども、GitHub Actionsのワークフローを使って成果物のアップロードを行っていた場合は問題がある。具体的には以下のワークフローについて

on:
  release:
    types:
    - published

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v5
    - run: make zip
    - uses: actions/upload-artifact@v4
      with:
        name: artifacts
        path: "*.zip"
  deploy:
    needs: build
    permissions:
      contents: write
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v5
    - uses: actions/download-artifact@v5
      with:
        path: artifacts
        merge-multiple: true
    - name: release
      run: |
        gh release upload "$TAG" artifacts/*.zip
      env:
        TAG: ${{ github.ref_name }}
        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

このとき、Immutable Releasesが有効になっていると gh release upload によるアップロードが行えなくなる。上にも書いたようにドラフトであれば成果物の追加が行えるけれども、ドラフトの状態でワークフローをトリガーするイベント が、少なくとも今の時点では存在しない*1

ではどうするのかというと、公式ドキュメントにUsing immutable releases and tags to manage your action’s releasesがあって、このドキュメントではGitのタグとして作る方法が紹介されていたのでこの方法でリリースを行うことにした。ワークフローの差分は以下の通り。

diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index ef97f43..c1db329 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,9 +1,9 @@
 name: Release
 
 on:
-  release:
-    types:
-    - published
+  push:
+    tags:
+    - v[0-9]+.[0-9]+**
 
 jobs:
   build:
@@ -47,7 +46,10 @@ jobs:
         merge-multiple: true
     - name: release
       run: |
-        gh release upload "$TAG" artifacts/*.zip
+        gh release create "$TAG" \
+          --title="$TAG" \
+          --generate-notes \
+          --draft artifacts/*.zip
       env:
         TAG: ${{ github.ref_name }}
         GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

リリースを行いたいときGitのタグをプッシュすれば、ワークフローによってドラフトのリリースが作られる。このとき、上記ワークフローではリリースノート生成と成果物のアップロードまで行うように変更しているが、リリースノートの内容は後からGitHubのUIで変更できるし、リリースと関連付けられていないタグなら削除できるので、タグ付けを間違えた場合はドラフトリリースとGitのタグそのものを削除してやり直せばいい。

ただ、誤って先にGitHubのUIでリリースとして作ってしまった場合は(もう消せないしアップロードもできないので)新しくタグを作るしかなくなってしまうけれども、Immutable Releasesを使わないよりはメリットが大きいだろうから諦める。




元の記事を確認する

関連記事