Go releases are a comparatively friendly target for SLSA provenance. The Go toolchain is deterministic in most configurations, cross-compilation is straightforward, and the standard release pattern (build binaries, attach them to a GitHub release, cut a tag) maps cleanly to what the SLSA framework expects. Compared to ecosystems with complex build graphs, Go's "one command, one binary per target" model removes most of the ambiguity that makes provenance generation painful.
This post is a practical guide to producing SLSA Build L3 provenance for Go releases. It covers the slsa-github-generator reusable workflows, integration with goreleaser for multi-target builds, verification via slsa-verifier, and the patterns we have converged on for publishing binaries that downstream consumers can trust. The examples assume Go 1.22+ and slsa-github-generator v1.10.
What does a basic Go SLSA release look like?
The simplest path is the slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml reusable workflow. A caller workflow invokes it with the module path, a build configuration, and an output artifact name. The reusable workflow builds the binary in an isolated environment that the caller cannot influence, generates SLSA Provenance v1 attestation, signs it via Sigstore keyless, and attaches the signed provenance to the GitHub release.
A minimal caller workflow looks approximately like:
jobs:
build:
permissions:
id-token: write
contents: write
actions: read
uses: slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml@v2.0.0
with:
go-version: 1.22
config-file: .slsa-goreleaser.yml
The configuration file is a YAML document that defines one or more build targets. Each target specifies the main package path, the output binary name, the target OS/architecture, environment variables for the Go build, and optional ldflags. The reusable workflow builds each target in isolation and emits one provenance per binary.
This flow reaches L3 because the reusable workflow runs in a separate isolated GitHub Actions environment, the signing identity is the reusable workflow itself (not the caller), and the provenance includes the caller's source repository and ref as external parameters. A verifier that pins the identity to slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml and checks the caller via the certificate's OIDC claims can confirm all three properties in one verification.
How does this integrate with goreleaser?
Most real Go projects use goreleaser to build multi-target matrices, create archives, generate checksums, and publish releases. goreleaser v1.24+ has built-in SLSA provenance support via the sbom and slsa sections of the configuration, and it integrates with the actions/attest-build-provenance@v1 action for GitHub-hosted signing.
The goreleaser flow is slightly different from the reusable workflow flow. Goreleaser runs inside the caller's workflow, which means the signing identity is the caller workflow, not a reusable workflow. This is only Build L2, because the build environment is not isolated from the tenant. For many projects L2 is acceptable; for projects that need L3 the reusable workflow path is the right one.
The pragmatic compromise we use in projects that want both goreleaser's flexibility and L3 provenance is a two-stage flow. The caller workflow runs goreleaser to produce binaries and archives. A second job calls the slsa-github-generator/builder_go_slsa3.yml reusable workflow with the already-built binaries as subjects to attest. This keeps goreleaser's build matrix but pushes the attestation generation into the isolated reusable workflow.
The downside is that the provenance now covers the attestation step, not the actual goreleaser build. External parameters record the release tag but not the goreleaser configuration, which limits the provenance's usefulness for reproducibility. It is a real trade-off, not a free lunch.
What about container images?
Go projects that publish container images typically use ko or a small Dockerfile-based build. For SLSA Build L3 provenance on the image, the tool to reach for is the slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml reusable workflow. It takes an image reference (already built and pushed to a registry) and generates provenance about that image.
The workflow does not build the image; it attests to an image someone else built. This is deliberate: the attestation generator and the image builder are different trust domains, and separating them reduces the surface area. In practice the pattern is: caller workflow builds the image with ko build (which is itself reproducible for Go), pushes to ghcr.io, and then invokes the generator workflow with the pushed image's digest.
The generator pushes the provenance attestation to the registry using the cosign referrers API (cosign 2.2+). A consumer can then run cosign verify-attestation --type=slsaprovenance1 --certificate-identity-regexp='^https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3\.yml' --certificate-oidc-issuer=https://token.actions.githubusercontent.com IMAGE to verify the provenance exists and was signed by the expected workflow.
How do consumers verify Go release provenance?
slsa-verifier is the tool built for this specifically. For a binary release, the verification command is slsa-verifier verify-artifact BINARY --provenance-path BINARY.intoto.jsonl --source-uri github.com/ORG/REPO --source-tag vX.Y.Z. The verifier checks that the binary's SHA256 matches the provenance's subject digest, that the provenance was signed by the expected reusable workflow, that the certificate's OIDC claims match the provided source URI and tag, and that the Rekor transparency log entry is valid.
For organisations consuming Go tools, the verification should happen once at ingestion, not at every use. A common pattern is a download proxy that fetches a release binary, runs slsa-verifier, and only publishes the binary to the internal mirror if verification passes. The output of the verification, typically a Verification Summary Attestation (VSA), can be cached and referenced by downstream consumers without re-running the full verification.
The operational detail that matters: slsa-verifier needs network access to Rekor to verify the transparency log entry. In air-gapped environments, use the --rekor-url flag to point at an internal Rekor mirror, and ensure the Sigstore TUF metadata is mirrored alongside.
What about Go module provenance?
A separate topic from binary releases is the provenance of Go modules as consumed by go get. Go modules are distributed through the module proxy at proxy.golang.org, and they are authenticated via the checksum database at sum.golang.org. This is a form of transparency log, separate from Rekor, that records the cryptographic hash of every module version ever requested through the proxy.
The sum database is not SLSA provenance. It is a tamper-evident record of what the module proxy observed, which is an important property but a different one. It does not say anything about how the module was built or by whom. For projects that need provenance about the source-to-module transformation, there is currently no standardised solution; the Go team has experimented with signed module manifests but nothing has shipped as of Go 1.22.
For consumers, this means a two-layer model: the sum database provides integrity for modules as published, and the SLSA provenance on release binaries provides integrity for the compiled artifacts. Both matter, and neither replaces the other.
How Safeguard Helps
Safeguard integrates slsa-verifier into artifact ingestion so every Go release binary and container image coming into your organisation is validated against expected builder identities and source repositories before it reaches internal mirrors. Our policy layer understands the reusable-workflow pattern and automatically pins identities for both builder_go_slsa3.yml and generator_container_slsa3.yml while checking caller context. When a release regresses from L3 to L2 because the pipeline switched away from the reusable workflow, we flag it before the artifact ships. Customers running internal Go module proxies can feed us sum database entries and we correlate them with release provenance to give a unified integrity view.