This example outlines how to securely release using GoReleaser and GitHub Actions.
We'll go over a few things: GitHub settings, GoReleaser configuration, and GitHub actions.
These are some things I recommend you do:
- General > Require contributors to sign off on web-based commits Loading
- General > Enable release immutability
- Actions > General > Require approval for all external contributors
- Actions > General > Read repository contents and packages permissions
- Rules > New Ruleset > Import a ruleset > this file
- Advanced Security > Private vulnerability reporting
- Advanced Security > Dependency graph
- Advanced Security > Automatic dependency sub 8000 mission
- Advanced Security > Dependabot security updates
There's much more you can change, these are the things I usually do.
The provided configuration is commented out and each section links to the relevant documentation, but here's a rundown:
- We build for a couple of platforms using the Go mod proxy;
- We create archives for both the binaries as well as for the source;
- We create and sign a checksums file (using Cosign);
- We create Software Bill of Materials (SBOMs) for all the archives (using Syft);
- all these files are uploaded to the GitHub release;
- We create a Docker image manifest, which also includes SBOMs;
- We then sign the image.
We have 3 workflows set up, let's go over them.
The build workflow doesn't do much: it checks
out the code, installs Go, and runs go test
.
The security workflow does a lot more, as it has a couple of jobs:
codeql
: as the name implies, runs the recommended CodeQL queries for Go and Actions;grype
: runs Grype, which scans for known vulnerabilities;govulncheck
: runs the standard Go vulnerability checker;dependency-review
: runs only on pull requests, and checks if any dependencies being added or updated are allowed.
All these jobs report their status using Static Analysis Results Interchange Format (SARIF), so any findings will show as security alerts in the Security > Code scanning tab.
Finally, the release workflow. Its main job is to, well, release our software, and it uses GoReleaser for that (surprise!). But to do that, we first set up Docker, Cosign, and Syft. Then, we run the glorious goreleaser-action, which does all the heavy lifting. Then, after all is said and done, we attest our build artifacts.
As you may have noticed, all the actions are pinned to the SHA1 of their respective tags.
This is recommended, as an attacker might take over an action and re-publish malicious code under the same tags.
Using only the major versions (like @v4
) is also not so good, as you are then
even more clueless about what is actually being run.
If you want to pin all the actions in your repositories, I recommend using pinata.
To create a new release, create and push a new tag. You can get the next semantic version using svu:
git tag -s $(svu n)
git push --tags
And then go over to the actions tab and wait for the release to finish.
Your users will need to know how to verify the artifacts, and this is what this section is all about.
The first thing we need to do, is get the current latest version:
export VERSION="$(gh release list -L 1 -R goreleaser/example-secure --json=tagName -q '.[] | .tagName')"
Then, we download the checksums.txt
and the signature bundle
(checksums.txt.sigstore.json
) files, and then verify them:
wget https://github.com/goreleaser/example-secure/releases/download/$VERSION/checksums.txt
wget https://github.com/goreleaser/example-secure/releases/download/$VERSION/checksums.txt.sigstore.json
cosign verify-blob \
--certificate-identity "https://github.com/goreleaser/example-secure/.github/workflows/release.yml@refs/tags/$VERSION" \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
--bundle "checksums.txt.sigstore.json" \
./checksums.txt
This should succeed - which means that we can from now on verify any artifact from the release with this checksum file!
You can then download any file you want from the release, and verify it with, for example:
wget "https://github.com/goreleaser/example-secure/releases/download/$VERSION/example_linux_amd64.tar.gz"
sha256sum --ignore-missing -c checksums.txt
Which should, ideally, say "OK".
You can then inspect the SBOM file to see the entire dependency tree of the binary, check for vulnerable dependencies and whatnot.
To get the SBOM of an artifact, you can use the same download URL, adding
.sbom.json
to the end of the URL, and we can then check it out with grype
:
wget "https://github.com/goreleaser/example-secure/releases/download/$VERSION/example_linux_amd64.tar.gz.sbom.json"
sha256sum --ignore-missing -c checksums.txt
grype sbom:example_linux_amd64.tar.gz.sbom.json
Finally, we can also use the gh
CLI to verify the attestations:
gh attestation verify \
--owner goreleaser \
*.tar.gz
Docker images are a bit simpler, you can verify them with Cosign and Grype directly, and check the attestations as well.
Signature:
cosign verify \
--certificate-identity "https://github.com/goreleaser/example-secure/.github/workflows/release.yml@refs/tags/$VERSION" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"ghcr.io/goreleaser/example-secure:$VERSION"
Vulnerabilities:
grype "docker:ghcr.io/goreleaser/example-secure:$VERSION"
Attestations:
gh attestation verify \
--owner goreleaser \
"oci://ghcr.io/goreleaser/example-secure:$VERSION"
If all these checks are OK, you have a pretty good indication that everything is good.
I really hope this helps - and please feel free to open PRs improving things you think need improving, or issues to discuss any concerns you might have.
Thanks for reading!