Introduction, practice and tutorial for achieving supply chain security in the container space.
Build the provided Dockerfile
as a
local container image:
docker build -t hello-container-signing .
Then run it:
docker run --rm hello-container-signing
To get started with container signing we'll need a copy
of the cosign
command-line tool so we can:
- Generate our own maintainer key-pair
- Verify container images
If you're on macOS, install cosign
as follows for the
easiest and fastest way:
brew install cosign
For other installation targets you may head over to releases in order to find the one relevant to your CPU architecture and particular need.
The cosign
tool accepts several methods of passing
the private password in order to generate a key-pair
type of key signature, such as:
- Pass it as STDIN:
echo "1234" | cosign generate-key-pair
- Type into an interactive standard interface:
cosign generate-key-pair
- Use an environment variable
COSIGN_PASSWORD=1234 cosign generate-key-pair
You may use whichever you prefer, and should output similar to the following:
Enter password for private key:
Enter again:
Private key written to cosign.key
Public key written to cosign.pub
Once the key-pair has been successfully generated, it will result in the following files created on disk, in the current working directory:
cosign.key
which is the private signing keycosign.pub
which is the public signing key
To allow for automated signing of container images built and published to a repository, we need to make the key-pair available as environment secrets on the GitHub Actions CI.
Head over to GitHub's repository settings at https://github.com/lirantal/dockly/settings/secrets/actions/new and created the following environment variables, with their respective values from the key-pair files that were generated earlier:
COSIGN_PUBLIC_KEY
- public key from the generated filecosign.pub
COSIGN_PRIVATE_KEY
- private key from the generated filecosign.key
COSIGN_PASSWORD
- the value of the password provided tocosign generate-key-pair
earlier
Now, that these set of sensitive information have been set on
the repository it is advised that you remove the private key
cosign.key
from the filesystem, so it isn't leaked by an
accidental commit to a public repository, mistakenly added to
a built container image, or some other reason for credential leak.
Furthermore, it is advised you keep a copy of the above sensitive information in the likes of a key management system, such as 1Password. You'll need access to all of them, including the public key.
Add the following to your GitHub Actions workflow, which handles the installation of the Cosign utility as part of the workflow, and then continues to sign the image:
- name: Cosign install
uses: sigstore/cosign-installer@v2.4.0
- name: Sign the published container image
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
TAGS: ${{ steps.meta.outputs.tags }}
run: cosign sign --key env://COSIGN_PRIVATE_KEY ${TAGS}
And here's a reference to a complete container image build and
publishing to the GitHub Packages container registry for the
open source project dockly
which serves as an example reference:
name: "Docker: GitHub Packages"
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
push:
branches: [ main ]
tags: [ 'v*.*.*' ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/dockly
jobs:
build_and_publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=true
prefix=
suffix=
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Cosign install
uses: sigstore/cosign-installer@v2.4.0
- name: Sign the published container image
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
TAGS: ${{ steps.meta.outputs.tags }}
run: cosign sign --key env://COSIGN_PRIVATE_KEY ${TAGS}
Once the GitHub Action workflow completes successfully in building and signing the image at the GitHub Packages container registry we can proceed to verifying the container image signature.
This step of verifying container image signatures is mostly reserved and to be used by end-users who consume the container images. Verifying Docker images or other OCI-compliant container images is performed in order to ensure that specific claims have been correctly signed and that overall provenance tested for integrity is correct
To verify the image, we will need access to the public key of the maintainer, which was used to sign the image. Since we have created this one in prior steps, it should be easily available on disk:
cosign verify ghcr.io/lirantal/dockly --key cosign.pub
This should output similar information to the following:
Verification for ghcr.io/lirantal/dockly:latest --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
[{"critical":{"identity":{"docker-reference":"ghcr.io/lirantal/dockly"},"image":{"docker-manifest-digest":"sha256:3cb3a861c4077c63a48a523c1ab74c0e6e737fae112b8f8602718509385a74fd"},"type":"cosign container image signature"},"optional":null}]
Tip: you can pipe the output of cosign verify
to the popular JSON
querying tool jq
and get a pretty-print version such as this:
cosign verify ghcr.io/lirantal/dockly --key cosign.pub | jq
Verification for ghcr.io/lirantal/dockly:latest --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
[
{
"critical": {
"identity": {
"docker-reference": "ghcr.io/lirantal/dockly"
},
"image": {
"docker-manifest-digest": "sha256:3cb3a861c4077c63a48a523c1ab74c0e6e737fae112b8f8602718509385a74fd"
},
"type": "cosign container image signature"
},
"optional": null
}
]
So far, we have confirmed that this specific container image tag was signed with the public key that we used. This is one attestation with regards to a claim about the identity who published this Docker image.
What if we also want to track further attestations, such as the source repository of this container image, and the specific build workflow origin and commit reference it is related to?
You can look up the relevant information you wish to include as attestations for the overall container image provenance in GitHub's Actions documentation about Contexts.
I have updated the signing clause of the workflow file with the following claims to be added to the signature:
- name: Sign the published container image
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
TAGS: ${{ steps.meta.outputs.tags }}
run: |
cosign sign --key env://COSIGN_PRIVATE_KEY ${TAGS} \
-a "repo=${{ github.repository }}" \
-a "workflow=${{ github.workflow }}" \
-a "ref=${{ github.sha }}" \
-a "actor=${{ github.actor }}" \
-a "build=${{ github.run_id }}"
Once the build completed successfully we can verify the container
image along with the new attestations added to it in the optional
field:
$ cosign verify ghcr.io/lirantal/dockly --key cosign.pub | jq
Verification for ghcr.io/lirantal/dockly:latest --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
[
{
"critical": {
"identity": {
"docker-reference": "ghcr.io/lirantal/dockly"
},
"image": {
"docker-manifest-digest": "sha256:2bbff3db48fcae016aa2d2116834723591a40335f47be6df81c532170b45776b"
},
"type": "cosign container image signature"
},
"optional": {
"actor": "lirantal",
"build": "2535552796",
"ref": "a40c60c8dc6d8d970431606b87df01dc71486b6c",
"repo": "lirantal/dockly",
"workflow": "Docker: GitHub Packages"
}
}
]