おそらくはそれさえも平凡な日々

GitHub Actions上で証明書を付与するサプライチェーン構築

とにかく数多のサプライチェーンインシデントが発生しており、OSSを公開・頒布している身からすると、より気を使わなければいけない状況です。

例えば、配布しているバイナリやアーカイブ、パッケージなどの成果物(artifact)の信頼性を保証するために、それが、どのソースを元に、どの環境及びワークフローでビルドされたか等の来歴 (provenance) を証明 (attest) したいところです。言ってしまえば「私が作りました」のような原産地証明に似た話です。

誰かの手元などのクローズドな環境でビルドされた成果物をアップロードして利用してもらう方法は、もはや信頼性が乏しく、推奨できなくなりました。現代では以下のようなサプライチェーンを構築することが望ましくなっています。

  • 入力となるソースツリーを明確にする
  • 独立性と再現性の高い決定的なビルド手順を整える
  • 透明性が高いオープンな環境でビルドを実施する
  • それらの情報を含め、成果物類に証明書 (attestation) を付与する
  • リリースをimmutableにする

これらのガイドラインとしてのフレームワークにSLSA (Supply-chain Levels for Software Artifacts) があります。そして、GitHub Actionsで適切な設定を行えば、Build TrackのLevel 3という現状の最高レベルを比較的容易に満たせます。

特に、 actions/attest という公式提供のアクションが便利かつ重要です。指定したビルド成果物に対して、ソースコードやビルドしたワークフローの情報を含むprovenance(来歴)のattestation(証明書)を生成し、署名してGitHubのattestation storeに登録してくれます。署名にはGitHub Actionsの実行時の認証情報とSigstore が発行した短命証明書を使うため、秘密鍵等を自分で用意・管理する必要もありません。

ドキュメント: Using artifact attestations to establish provenance for builds - GitHub Docs

解説は以下の記事にも書かれており、参考になります。ただ、少し古い内容もある為、現状の最新情報をもとに実例を含めて解説します。

Enhance build security and reach SLSA Level 3 with GitHub Artifact Attestations

GitHub Actions上での実装方針

先程の4項目をGitHub上で実現する具体的な対応は以下のようになります。

  • 入力のソースツリーの明確化
    • → ビルドの入力となるGitリポジトリのref(主にタグ)を指定
  • ビルド手順の整備
    • → 独立したreusable workflowを作成
  • オープンな環境でのビルド
    • → パブリックなGitHub Actions上でビルド
  • 成果物類へのattestationの付与
    • → GitHub公式の actions/attest カスタムアクションを使う
  • リリースをimmutableにする
    • → GitHubのリポジトリ設定でimmutable releasesを有効化する

ビルド手順は、極力再現性の高いものを整えたいところです。後の方でも少しだけ解説しますが、これを厳密にやろうとすると難しいため、本エントリでは詳しくは触れません。

attestationの付与にあたっては actions/attest-build-provenance が紹介されている記事もありますが、これは古いActionであり、現在は actions/attest に統合され、置き換わっているため、新しいプロジェクトではこちらを使うようにして下さい。

また、成果物のリリースに当たっては、そのリリースは改竄防止のために極力immutableにしましょう。多くのパッケージリポジトリは、一度公開されたリリースはその内容を変更できないようになっていますが、GitHubもリポジトリにimmutable releases設定をすることで、リリースをimmutableにできるので、積極的に有効にしましょう。

関連:

reusableワークフローと actions/attest を使ったリリースフローの実例

ここでは、私がGoの実行バイナリとリリースに用いている実際のワークフローを少し簡易化した物を例に解説します。実際のフローは Songmu/tagpr リポジトリも併せてご参照下さい。

このワークフローはリリース対象のタグをrefに指定して起動します。release.yaml と release-build.yaml の2つのworkflowに分かれています。release.yaml がreusableワークフローである、release-build.yaml を workflow_call で呼び出してビルドを行い、そこで作られたzipやtar.gzの成果物を元に、リリースを作成する構成です。

reusableワークフローに分離しているのは、ビルドの独立性を高める為で、これはSLSAのLevel 3要件を満たす為にも必要です。

# .github/workflows/release.yaml
name: release
on:
  workflow_dispatch:

jobs:
  build:
    if: github.ref_type == 'tag'
    permissions:
      attestations: write
      contents: read
      id-token: write
    uses: ./.github/workflows/release-build.yaml

  publish:
    needs: build
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      contents: write
    steps:
    - name: download release artifacts
      uses: actions/download-artifact@<ref>
      with:
        name: release-${{ github.ref_name }}
        path: release
    - name: Publish GitHub release
      env:
        GH_TOKEN: ${{ github.token }}
        TAG: ${{ github.ref_name }}
      run: gh release create "$TAG" release/* --repo "$GITHUB_REPOSITORY" --verify-tag --generate-notes       
# .github/workflows/release-build.yaml
name: release build

on:
  workflow_call:

jobs:
  build:
    runs-on: ubuntu-26.04
    timeout-minutes: 10
    permissions:
      attestations: write
      contents: read
      id-token: write
    steps:
    - name: checkout
      uses: actions/checkout@<ref>
      with:
        persist-credentials: false
    - name: setup go
      uses: actions/setup-go@<ref>
      with:
        go-version-file: go.mod
    - name: build release archives
      id: goxz
      uses: Songmu/goxz@<ref>
      with:
        package-version: ${{ github.ref_name }}
        os: linux,darwin,windows
        arch: amd64,arm64
        packages: ./cmd/tagpr
        build-ldflags: -s -w
        checksum: "true"
        static: "true"
    - name: attest build provenance
      uses: actions/attest@<ref>
      with:
        subject-path: "${{ steps.goxz.outputs.output-dir }}/*"
    - name: upload release artifacts
      uses: actions/upload-artifact@<ref>
      with:
        name: release-${{ github.ref_name }}
        path: "${{ steps.goxz.outputs.output-dir }}/*"
        if-no-files-found: error
        retention-days: 1

図示するとこんな具合です。

ここでは、Goの例ですが、ビルド環境のセットアップやビルド手順の部分を差し替えれば、他言語でも簡単に実現できます。また、Goのビルドにも Songmu/goxz というカスタムアクションを使っていますが、GoReleaser を使ってもよいですし、そちらのほうが無難でしょう。

ビルドの再現性 (reproducibility) を高める

ここでは、Gitのタグのみを入力として、そのタグに紐付いたソースツリーからビルドを行っています。理想的には、同じタグをチェックアウトして同じ環境を整え、同様のビルド手順を実行すれば、必ず同一の成果物を得られるような、再現可能(reproducible)なビルドワークフローの構築を目指す必要があります。

これも厳密にやろうとすると難しく、解説しきれないので、reproducible-builds.org などを参考にしてください。例えば、以下のような対応を、現実的に可能な範囲で極力実施するのが良いでしょう。

  • 言語・ライブラリのバージョンロック
  • ビルド環境の固定
    • OSやrunnerのバージョン指定
    • カスタムGitHub Actionsのpin留め
    • コマンドラインツールのバージョン固定
  • アーカイブ作成方式の固定
    • 圧縮アルゴリズムやオプションの固定
    • タイムスタンプ、ユーザー、パーミッションの固定

これらに関連し、私もビルドに使っているCLIである goxz を、カスタムGitHub Actionsとしても提供してpin留めしやすくしたり、zipやtar.gzのアーカイブ生成の再現性を高める対応を行うなどしました。

また、Perlのディストリビューション作成を行うMinillaでも、tar.gzのアーカイブ生成の再現性を高める対応を行いました。

実装してみると、アーカイブを再現可能にするのは地味に大変で、厳密にやるのも難しいことが分かったので、既存のツールのありがたみが分かりました。

成果物を検証する

actions/attest で生成される証明書は、署名付きのJSONファイルで、成果物のハッシュ値やビルドに使ったソースコードのGitリポジトリの情報、ワークフローの情報などが含まれています。https://github.com/Songmu/tagpr/attestations/ などのURLから内容を確認できます。

これを用いて、成果物を検証できます。検証には gh attestation verify コマンドを使うのが簡単です。例えば、以下のように実行することで、その成果物が、指定したGitリポジトリの指定したコミットから、意図したワークフローを使ってビルドされたことを検証できます。

$ gh attestation verify "$artifact.tar.gz" \
    --hostname github.com \
    --repo "$REPO" \
    --source-digest "$commit" \
    --signer-digest "$commit" \
    --signer-workflow "$REPO/.github/workflows/release-build.yaml"  

以前の それなりに安心な install.sh を作るツール insmith の記事でも触れましたが、insmith 内部ではこのコマンドを使ってGoのバイナリの検証をしています。

まとめ

  • actions/attest で、GitHub Actions上でお手軽に成果物に証明書を付与できるので利用していきましょう
  • 再現性のあるビルドワークフローを整備し、透明性の高い環境で実行しましょう
created at
last modified at

2026-10-11T18:22:39+0900

comments powered by Disqus