Safeguard your containers with new container signing capability in GitHub Actions

Image of Justin Hutchings

As developers have leaned into cloud native projects for scale and maintainability, the popularity of containers has exploded. With 92% of organizations leveraging containers in production, it’s safe to say they are here to stay. Unfortunately, so are their security risks. Most containers available today are vulnerable to supply chain attacks, because they can be published with nothing more than a simple API key. If that key leaks, it’s easy for an attacker to publish a legitimate looking container that actually contains malware. One of the best ways to protect users from these kinds of attacks is by signing the image at creation time so that developers can verify that the code they received is the code that the maintainer authored.

GitHub is a founding member of the Open Software Security Foundation (OpenSSF), an industry group focused on producing shared solutions to protect developers from open source security risks. One of the really exciting open source security projects now sponsored by the OpenSSF is sigstore, which allows developers to securely build, distribute, and verify signed software artifacts.

Today, we’re happy to announce that we have integrated sigstore support for container image signing into the GitHub Actions starter workflow, so that developers can sign their container images by default. Leveraging this workflow gives your users confidence that the container images they got from their container registry was the trusted code that you built and published.

So how does container signing with GitHub Actions work?

Let’s start our journey with the sigstore tool for signing container images, cosign. It supports several types of signing keys and also supports adding key-value annotations to the signature (more on this later).

To keep our example simple, let’s say you have a key you generated with $ cosign generate-key-pairthat you’ve saved in your GitHub Actions repository secrets called SIGNING_SECRET. Then, in your Actions workflow, after you’ve built the container image, you’d sign it by doing something like:

...

jobs:
  build:
    steps:
      # ... build steps here

      - uses: sigstore/cosign-installer@main

      - name: Write signing key to disk (only needed for `cosign sign --key`)
        run: echo "${{ secrets.SIGNING_SECRET }}" > cosign.key

      - name: Sign container image
        run: |
          cosign sign --key cosign.key \
            ghcr.io/your-org/your-repo:some-tag
        env:
          COSIGN_PASSWORD: ""

That’s it! That’s all you have to do to sign your container image in GitHub Actions and store it in GitHub Packages (or any of the other container registries cosign supports as well). You can then verify the signature when you pull the image, before a deploy:

$ cosign verify --key cosign.pub ghcr.io/your-org/your-repo:some-tag

Verification for ghcr.io/your-org/your-repo:some-tag --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
  - Any certificates were verified against the Fulcio roots.

[{"critical":{"identity":{"docker-reference":"ghcr.io/your-org/your-repo"},"image":{"docker-manifest-digest":"sha256:..."},"type":"cosign container image signature"},"optional":{}}]

That’s pretty cool, but what if I wanted to include attestations into that signature, so I could track that this build came from the repository, workflow, and commit reference that I was expecting? Let’s tweak our signing step to add in those properties as annotations into the signature:

      - name: Sign container image with annotations from our environment
        run: |
          cosign sign --key cosign.key \
            -a "repo=${{ github.repository }}" \
            -a "workflow=${{ github.workflow }}" \
            -a "ref=${{ github.sha }}" \
            ghcr.io/your-org/your-repo:some-tag
        env:
          COSIGN_PASSWORD: ""

That’s easy, right? Let’s see what our new signature looks like (under “optional”):

$ cosign verify --key cosign.pub ghcr.io/your-org/your-repo:some-tag

Verification for ghcr.io/your-org/your-repo:some-tag --
...
[{"critical":{"identity":{"docker-reference":"ghcr.io/your-org/your-repo"},"image":{"docker-manifest-digest":"sha256:..."},"type":"cosign container image signature"},"optional":{"ref":"01234564789abcdef...","repo":"your-org/your-repo","workflow":"Cosign Example"}}]

There are many other context variables you could pull in, depending on what properties you want to validate before deploying.

It’s important to note another part of sigstore is fulcio, a root CA that issues signing certificates from OIDC tokens, as well as Rekor, a transparency log for certificates issued by fulcio. In October, we announced that Actions runs can get OIDC tokens from GitHub for use with cloud providers, which includes the public fulcio and Rekor servers run by the sigstore project.

This means you can sign your container images with the GitHub-provided OIDC token in Actions, without provisioning or managing your own private key. This is critically what makes signing so easy that we can turn it on by default, because there’s no additional setup beyond adding a specific GitHub Actions workflow.

This keyless signing includes Rekor, a public transparency log, meaning your username, organization name, repository name, and workflow name will be published to the public transparency log. We believe this is the right choice for public repositories, but have disabled this in private repositories by default to prevent potential leaks of the name of the private repository to a third-party service.

Get started with container signing

Do you want to get started? Check out the starter workflow template we’ve updated to take advantage of keyless signing when run from a public repository. If you have an existing Actions workflow, you can simply add the new cosign steps to your existing workflows.

With that, we’d like to wish you all a very happy code signing! A big thank you to the sigstore project, the folks at Chainguard that contributed this code, and to our colleagues in the OpenSSF for their ongoing work in securing the open source ecosystem.