Container image signing arrives in GitHub Actions

Container adoption keeps climbing—92% of organizations now run containers in production—and so does the attack surface. Publishing a container today can be as simple as holding an API key; if that key leaks, an attacker can push official-looking images packed with malware. Signing images at build time gives consumers a way to confirm the image they pull is exactly what the maintainer published.

GitHub, as a founding member of the Open Software Security Foundation (OpenSSF), has been pushing shared security tooling for the open source ecosystem. One of the most promising OpenSSF-sponsored projects is sigstore, a set of tools for building, distributing, and verifying signed software artifacts. GitHub has now baked sigstore support for container signing into its GitHub Actions starter workflow, making image signing available by default.

Signing with cosign

The sigstore tool for signing container images is cosign, which supports several key management options as well as key-value annotations attached to signatures. The simplest path starts with a key generated via $ cosign generate-key-pair, stored as a repository secret named SIGNING_SECRET. After building the image in an Actions workflow, add a signing step:

...

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: ""

The signed image can be pushed to GitHub Packages or any other registry cosign supports. Before deploying, verify the signature against the pulled image:

$ 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":{}}]

Signatures can also carry attestations about the build itself. By pulling in GitHub Actions context variables as annotations, the signature records exactly where the image came from—repository, workflow, and commit reference:

      - 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: ""

The resulting signature metadata is visible under “optional” when inspecting the image:

$ 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"}}]

Other context variables can be added depending on which properties you want to validate pre-deploy.

Keyless signing through OIDC

Cosign is only one piece of sigstore. The project also includes fulcio, a root CA that issues signing certificates from OIDC tokens, and Rekor, a transparency log for certs issued by fulcio. In October, GitHub announced that Actions runs can request OIDC tokens from GitHub for cloud providers; those tokens also work with the public fulcio and Rekor instances run by the sigstore project.

That opens the door to keyless signing inside Actions: use the GitHub-provided OIDC token for signing, with no private key to provision or manage. The only setup required is adopting the specific GitHub Actions workflow. The tradeoff to know: keyless signing publishes your username, organization, repository name, and workflow name to the public transparency log. That’s acceptable for public repositories, but GitHub disables keyless signing in private repositories by default to prevent leaking repository names to third-party services.

Getting started

The updated docker-publish starter workflow already enables keyless signing when run from a public repository. Existing Actions workflows can adopt the new cosign steps without a full rewrite.