Supply chain security has become an increasingly important topic in software delivery, this is primarily due to the increase in attacks on software supply chains and the need to verify software artifacts.
Supply chain security, in essence, refers to the measures and strategies implemented to safeguard the integrity of the processes involved in the development and distribution of software products.
In the context of supply chain security, software artifacts refer to the various components, such as code, libraries, configurations, and documentation, that make up a software product.
One of the more popular examples of supply chain attacks is the infamous SolarWinds hack, where the attackers were able to create a back door to the SolarWinds software, and this eventually made its way into the hands of consumers who were now running a malicious version of the software and remained undetected for months.
This was a huge wake-up call to organizations who weren’t already thinking about the security of their software delivery pipelines.
In this tutorial, we will take a brief look at supply chain attacks and security, plus how these can partially be mitigated by automatically signing container images using Cosign and GitHub actions.
What are supply chain attacks?
To further illustrate how supply chain attacks can occur, let's take a look at what a typical software delivery pipeline would look like.
The illustration above is a simplified example, where a developer pushes code to a source code repository such as GitHub. The code then triggers a build process in a Continuous Integration (CI) service where the code is built and bundled with its dependencies, and produces an artifact. This artifact might be a Python package, binary, or container image that can be deployed in the cloud.
This setup works fine, assuming we can trust every step in the delivery process. But what happens when an attacker gains access to any part of the delivery pipeline?
In the illustration above, the attacker has gained access to the build server, perhaps through a compromised SSH key or an undisclosed zero-day exploit. They can now modify the build process to include some malicious code producing a malicious artifact.
This leads to two big questions. How can authors sign artifacts such that their authenticity can be verified? How can consumers be sure they are running the legitimate version of the software?
What is the sigstore project and Cosign?
From sigstore.dev
sigstore was started to improve supply chain technology for anyone using open source projects. It's for open source maintainers, by open source maintainers. And it's a direct response to today’s challenges, a work in progress for a future where the integrity of what we build and use is up to standard.
Cosign is part of the suite of tools the Sigstore project built to provide a secure and transparent way of signing and verifying container images and artifacts. It is built with security and usability in mind and can be easily integrated into existing workflows. Cosign also provides key and signature transparency, allowing for easy verification of signatures and ensuring that no one can tamper with signed images without being detected.
Now that we have a good idea of the problem the Sigstore project and Cosign are trying to address, let's dive into the tutorial.
Prerequisites
This tutorial assumes some familiarity with GitHub actions in addition, you would need the following:
- A GitHub personal access token with write access
- A fresh GitHub Repository
- Go (1.19+) installed
Installing Cosign
If you have Go (1.19+) installed, installing Cosign is straightforward. In your terminal, run the the following command.
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
In a few minutes, you should have cosign installed. You can verify this by running
cosign version
Automating Container Image Signing
Generating a key pair
In order to sign our container images, we need to generate a public and private key pair. Luckily Cosign ships with a command to do this, and in fact, takes this a step further by being able to write the key pair directly to GitHub secrets. If you haven’t done so early already, clone the repository you created earlier onto your machine, and change into that directory in your terminal.
Before running the command, we need to export some environment variables:
export GITHUB_TOKEN=ghp_yourgithubtoken
export COSIGN_PASSWORD=areallysecurepassW0rd
Cosign will attempt to write the key pair to GitHub secrets, so we provide it with a GitHub token as well as a password for the private key.
Be sure to replace COSIGN_PASSWORD
with a password of your choosing.
Now you can run the following command, replacing the $REPO_OWNER
with your GitHub name, and the $REPO_NAME
with the name of your repository:
cosign generate-key-pair github://$REPO_OWNER/$REPO_NAME
This will also write the public key and an encrypted version of the private key to the current working directory.
We can verify Cosign has written the key pair by checking the secrets tab in the settings page of the repository on GitHub.
While in the secrets tab, create a new secret called GITHUB_TOKEN
and set the value to the Github token you generated. This would be required to publish our image to the GitHub container registry.
Configuring GitHub Actions
Next, let’s set up the GitHub workflow that will sign and publish images. Within your repository, create the directories github/
and under it workflows
:
mkdir -p .github/workflows
Next, create a workflow file:
touch .github/workflows/ci.yaml
Create and open up ci.yaml
in the editor of your choice and follow along with the code below:
name: build and sign
on:
push:
branches:
- 'main'
jobs:
build-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
name: build-image
steps:
- uses: actions/checkout@v3.5.2
with:
fetch-depth: 1
- name: Install Cosign
uses: sigstore/cosign-installer@v3.1.1
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v2.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# publish dockerfile in the repo
- name: Publish Dockerfile
uses: docker/build-push-action@v4
id: build-and-push
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm/v7,linux/arm64
push: true
tags: ghcr.io/${{ github.repository }}:latest
- name: Sign image with a key
run: |
cosign sign --yes --key env://COSIGN_PRIVATE_KEY ghcr.io/${{ github.repository }}:latest
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
The important part to look out for here is where we sign the image:
- name: Sign image with a key
run: |
cosign sign --yes --key env://COSIGN_PRIVATE_KEY ghcr.io/${{ github.repository }}:latest
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
This passes the secrets created earlier as an environment variable to the cosign command.
Creating a Dockerfile
With the workflow in place, we need an image to publish. Within the root directory of the repository, create a Dockerfile
and add the following instructions:
FROM alpine:latest
RUN apk update && apk upgrade
RUN apk add cowsay --repository <http://dl-3.alpinelinux.org/alpine/edge/testing/> --allow-untrusted
RUN apk add cowsay
CMD ["cowsay", "Hello, Docker!"]
While this might be a contrived example, the idea here is your project would typically have a Dockerfile that is used to build your application. Commit the files and push them to the repository to trigger a build.
Verifying signatures
As a consumer, it is important that you verify software artifacts before use, this ensures you are only using trusted artifacts. Cosign ships with a command that can be used to verify the integrity of the container image we published.
To verify the signature, run the following command:
cosign verify ghcr.io/$REPO_OWNER/$REPO_NAME --key cosign.pub -o text
If this succeeds, you should see the following output :
Verification for ghcr.io/$REPO_OWNER/$REPO_NAME:latest –
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The existence of the claims in the transparency log was verified offline
- The signatures were verified against the specified public key
Summary
In this tutorial, we demonstrated how to sign container images using Cosign using GitHub actions, we also highlighted the importance of supply chain security.
It's important to note that while signing your artifacts is a critical step, it is just one of the many layers that should be integrated into your software delivery pipeline. As a consumer, it is imperative to ensure that you are utilizing signed or verified software artifacts to enhance overall security and trustworthiness.
Additional Resources
Cosign supports a variety of signing methods that this post doesn’t cover, you can check them out here.
Cosign also allows you to store keys in various secret management solutions such as Hashicorp Vault, this is ideal for production environments.