Zero-friction “keyless signing” with Github Actions
For more background on “keyless signing”, see our previous posts on Fulcio and keyless signing with EKS. Here we will walk through how to apply these concepts to Github’s recently launched support for OpenID Connect (OIDC) tokens in Actions.
Enable OIDC
In order to enable Github Actions OIDC tokens you will need to make sure your actions workflow has the following permissions:
block (more info):
jobs:
release_job:
runs-on: ubuntu-latest
permissions:
id-token: write # This is the key for OIDC!
The id-token: write
line enables our release_job
to create tokens as this workflow. It is notable that this permission may only be granted to workflows on the main repository, so it cannot be granted during pull request workflows. To facilitate OIDC support several environment variables are injected that tell the workflow how to get these tokens (read more here).
Install Cosign
The next thing we need to do is install cosign
, which is a CLI supported by the sigstore community to make signing easy. The community has an action available through the Github Action Marketplace, which can be added to your workflows via:
- name: Install cosign
uses: sigstore/cosign-installer@main
You can configure this to use a particular release of cosign
(e.g. v1.3.1
) with:
- name: Install cosign
uses: sigstore/cosign-installer@main
with:
cosign-release: 'v1.3.1'
Signing Container Images
To sign container images, the last thing we need to do is add a step that invokes the cosign
CLI to sign the container image digest (replace << INSERT DIGEST >>
with:
- name: Sign the container image
env:
COSIGN_EXPERIMENTAL: "true"
run: cosign sign << INSERT DIGEST >>
Note: If the image you are signing is private, then you will need to pass --force
if you want to record signatures to the Rekor transparency log. Prior to cosignv1.4.x
this is required for keyless signing of private images.
The most common use case here is to sign something you built earlier in the workflow, so for example using the docker/build-push-action
step (e.g. id: push-step
) you can use that step’s output to access the digest like this:
- name: Sign the container image
env:
COSIGN_EXPERIMENTAL: "true"
run: |
cosign sign \
${REPO}@${{ steps.push-step.outputs.digest }}
Note: A common mistake here is to sign the tag you just pushed. Tags are mutable and can point to different image digests over time, so if you use the tag here you are opening yourself up to both race conditions and malicious actors (incl. the registry!) which could have you sign something other than what you just pushed. By signing the digest you just pushed, you effectively eliminate the need to trust the registry because you are signing a crypographically verifiable checksum of the image.
However, the docker/build-push-action
currently has a bug, so you will need to add the following step earlier in your workflow to avoid this:
- name: Setup buildx
uses: docker/setup-buildx-action@main
Putting it together
I have put together a small demo repo to show all of these pieces working together. You can try this out yourself by forking this repo, and triggering this workflow from your fork’s Actions tab.
If you take the resulting image digest and run:
COSIGN_EXPERIMENTAL=true cosign verify $DIGEST | jq .
You should see (snipped for brevity):
Verification for $DIGEST --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- Any certificates were verified against the Fulcio roots.
[
{
"critical": {
"identity": {
"docker-reference": "ghcr.io/mattmoor/zero-friction-actions"
},
"image": {
"docker-manifest-digest": "..."
},
"type": "cosign container image signature"
},
"optional": {
...
"Issuer": "https://token.actions.githubusercontent.com",
"Subject": "https://github.com/mattmoor/zero-friction-actions/.github/workflows/docker-publish.yml@refs/heads/main"
}
}
]
In particular, note the Github Actions "Issuer"
, and how the "Subject"
is the actions workflow used to sign the image!
Bonus: More than just containers!
While containers are an increasingly prominent type of artifact, they are not the only game in town! Thankfully the concepts we discussed here apply to most forms of artifacts, and we are seeing the Sigstore community working to integrate with all manner of artifacts.
In the Go ecosystem, the popular goreleaser project recently added support for keyless signing (example repo, blog). In the Ruby ecosystem, Shopify is investing in signing rubygems. In the Python ecosystem, there is PEP-480 trying to address package signing.
If you are interested in getting involved, or learning more about sigstore, please reach out via slack, email, or join the weekly community call.
Ready to Lock Down Your Supply Chain?
Talk to our customer obsessed, community-driven team.