GoReleaser with Cosign Signing and Syft SBOM

Signed Builds and Supply Chain Security for Go Projects

Posted by Craig Johnston on Wednesday, February 11, 2026

GoReleaser automates building, packaging, and publishing Go binaries. This article covers a complete configuration that adds cryptographic signing with Cosign and software bill of materials (SBOM) generation with Syft. Every artifact your project ships can be verified by consumers as authentic and unmodified.

This configuration pairs well with the verification pipeline in AI on a Leash for Go. The build-check target in that article’s Makefile validates the GoReleaser configuration locally before CI runs it for real.

Complete GoReleaser Configuration

Create .goreleaser.yml in your project root:

# .goreleaser.yml - Signed releases with SBOM
version: 2

before:
  hooks:
    - go mod tidy
    - go generate ./...

builds:
  - id: myproject
    main: ./cmd/myproject
    binary: myproject
    env:
      - CGO_ENABLED=0
    goos:
      - linux
      - darwin
      - windows
    goarch:
      - amd64
      - arm64
    ldflags:
      - -s -w
      - -X main.version={{.Version}}
      - -X main.commit={{.Commit}}
      - -X main.date={{.Date}}

archives:
  - format: tar.gz
    name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
    format_overrides:
      - goos: windows
        format: zip

checksum:
  name_template: "checksums.txt"

signs:
  - cmd: cosign
    artifacts: checksum
    args:
      - "sign-blob"
      - "--yes"
      - "--output-signature=${signature}"
      - "${artifact}"

sboms:
  - artifacts: archive
    cmd: syft
    args: ["${artifact}", "--output", "spdx-json=${document}"]

changelog:
  sort: asc
  filters:
    exclude:
      - "^docs:"
      - "^test:"
      - "^chore:"

release:
  github:
    owner: "{{ .Env.GITHUB_REPOSITORY_OWNER }}"
    name: "{{ .ProjectName }}"
  draft: false
  prerelease: auto

Signing with Cosign

Cosign signs your release checksums using keyless signing (via Sigstore). Anyone can verify that the binary they downloaded was built from your CI pipeline, not injected by a compromised mirror or supply chain attack. The --yes flag enables non-interactive mode for CI.

Users can verify cryptographic proof that the binary came from your CI pipeline, not a compromised mirror.

SBOM Generation with Syft

Syft generates a Software Bill of Materials in SPDX JSON format for each archive. An SBOM lists every dependency baked into the binary: name, version, license. When a CVE drops against a library, consumers of your binary can check their SBOM to see if they’re affected without reading your source code.

SBOM generation is increasingly required for government and enterprise software procurement. Adding it now costs nothing.

Build Configuration Details

CGO_ENABLED=0 produces statically linked binaries. No glibc dependency, no shared library issues across Linux distributions. The binary runs anywhere.

ldflags: -s -w strips debug symbols and DWARF information, reducing binary size by 20-30%.

Version injection via -X main.version={{.Version}} embeds the git tag into the binary. Your binary can report its own version with myproject --version.

Cross-compilation to six targets (linux/darwin/windows times amd64/arm64) happens in a single command. Go’s cross-compilation support makes this trivial.

The before hooks run go mod tidy and go generate to ensure the build starts from a clean state. If go mod tidy changes go.sum, your commit was incomplete.

Version Injection in Practice

To use the injected version variables, add this to your main.go:

package main

import "fmt"

var (
    version = "dev"
    commit  = "none"
    date    = "unknown"
)

func main() {
    fmt.Printf("myproject %s (commit: %s, built: %s)\n", version, commit, date)
    // ... rest of application
}

During development, these variables hold their default values. GoReleaser replaces them at build time with the actual git tag, commit hash, and build timestamp. This is the standard pattern for Go CLI tools, and the AI should follow it when generating new cmd/ entrypoints.

Validating Locally

Add a build-check target to your Makefile:

build-check:
	go build ./...
	go mod verify
	goreleaser check
	goreleaser release --snapshot --clean --skip=publish,sign,sbom

goreleaser check validates your configuration file. The snapshot build runs the full pipeline without publishing, so you catch broken build configs locally instead of in CI.

Installation

# GoReleaser
go install github.com/goreleaser/goreleaser/v2@latest

# Signing and SBOM (install via package manager or binary download)
# cosign: https://docs.sigstore.dev/cosign/system_config/installation/
# syft: https://github.com/anchore/syft#installation

Pin these to specific versions in your CI configuration. Using @latest is fine for local development, but CI should be reproducible.

Note: This blog is a collection of personal notes. Making them public encourages me to think beyond the limited scope of the current problem I'm trying to solve or concept I'm implementing, and hopefully provides something useful to my team and others.

This blog post, titled: "GoReleaser with Cosign Signing and Syft SBOM: Signed Builds and Supply Chain Security for Go Projects" by Craig Johnston, is licensed under a Creative Commons Attribution 4.0 International License. Creative Commons License