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カスタムアクションを使う
- → GitHub公式の
- リリースを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のアーカイブ生成の再現性を高める対応を行うなどしました。
- Add goxz GitHub Action by Songmu · Pull Request #51 · Songmu/goxz
- Make ZIP and tar.gz archives reproducible by Copilot · Pull Request #56 · Songmu/goxz
また、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上でお手軽に成果物に証明書を付与できるので利用していきましょう- 再現性のあるビルドワークフローを整備し、透明性の高い環境で実行しましょう