🐧

GoReleaser でビルドとリリースを分離して Supply Chain Security 改善

に公開

本記事では GoReleaser で Go の CLI をリリースする際に build と release を分離して Supply Chain Security をさせた話を紹介します。

GoReleaser は Go で CLI をリリースする際に広く使われており、自分も愛用しています。
設定ファイルを用意して goreleaser release コマンドを実行するだけでビルドからリリースまで一気通貫して行えます。

しかし、一つの CLI で一気通貫して行うというアプローチはセキュリティ的な課題もあります。
なぜなら、署名やリリースに必要な secret を build process に渡さなければならず、 build が侵害されることで secret が悪用されるリスクがあるからです。
GitHub Actions の場合、 ${{ github.token }} の権限は job 単位で設定するため、 GoReleaser を実行する job の ${{ github.token }}contents: writeid-token: write, attestations: write などの権限を渡す必要があり、 ${{ github.token }} が渡される任意の step が侵害されることでそれらの権限が悪用されるリスクがあります。
contents: write 権限が悪用されれば、過去のバージョンのすり替えも含め悪意のある asset をリリースされる可能性さえあります。
そこで、そういった secret, 権限は secure な必要最小限の process にのみ渡すことでセキュリティリスクを軽減することができます。

これは SLSA の Build L3 の要件でもあります。

https://slsa.dev/spec/v1.1/requirements

  • Any secret material used for authenticating the provenance, for example the signing key used to generate a digital signature, MUST be stored in a secure management system appropriate for such material and accessible only to the build service account.
  • Such secret material MUST NOT be accessible to the environment running the user-defined build steps.
  • It MUST NOT be possible for a build to access any secrets of the build platform, such as the provenance signing key, because doing so would compromise the authenticity of the provenance.

https://slsa.dev/spec/v1.1/threats

Steal cryptographic secrets (Build L3)
Threat: Use or exfiltrate the provenance signing key or some other cryptographic secret that should only be available to the build platform.
Mitigation: Builds are isolated from the trusted build platform control plane, and only the control plane has access to cryptographic secrets.
Example: Provenance is signed on the build worker, which the adversary has control over. Adversary uses a malicious process that generates false provenance and signs it using the provenance signing key. Solution: Builder generates and signs provenance in the trusted control plane; the worker has no access to the key.

Forge values of the provenance (other than output digest)
Example 2 (Build L3): Provenance is generated in the trusted control plane, but workers can break out of the container to access the signing material. Solution: Builder is hardened to provide strong isolation against tenant projects.

自分の場合 Go で CLI をリリースする際に以下のようなことをやっています。

  • CLI をビルド
  • go-licenses でライセンスを生成
  • checksum を生成
  • cosign で checksum を署名
  • SBOM の生成
  • Homebrew Cask の生成
  • scoop app manifest の生成
  • Winget に PR を作成 (aqua のみ)
  • GitHub Artifact Attestations の生成
  • SLSA Provenance の生成
  • GitHub Release を生成し、 asset をアップロード

SLSA Provenance に関しては、 slsa-github-generator が build job と独立した job で実行するように設計されているため元々問題ありませんでした。

Homebrew や scoop, Winget をローカルからリリース

これは以前ブログに書いた通り、元々 CI で実行するのはやめているため、問題ありませんでした。

https://zenn.dev/shunsuke_suzuki/articles/github-security-2025#goreleaser-で生成した-homebrew-tap-や-scoop-manifest-などを-ci-でリリースする代わりにローカルからリリース

build job を複数の Job に分割

元々 GitHub Actions の build job で CLI のビルドだけではなく Cosign による署名、 Attestation の生成、 GitHub Release の作成及び asset の release なども行っていました。

Cosign による署名には id-token: write permission, attestation の生成には id-token: write 及び attestations: write, GitHub Release の作成及び asset の release には contents: write 権限が必要になりますが、ビルドにこれらの権限は不要です。

そこでこれらの処理を build job とは別の job に分離するようにしました。
これにより build job が侵害されたときのリスクを最小限に抑えることができるようになりました。

この workflow は Reusable Workflow として自分がメンテする全ての CLI で利用されているため、全ての CLI のセキュリティを高めることできました。

https://github.com/suzuki-shunsuke/go-release-workflow/blob/d13097c9dbb3e199e90c4e8ee45d852c5b7d86ed/.github/workflows/release.yaml#L31-L209

Discussion