Blog.

Secure your software supply chain using Sigstore and GitHub actions

Marco Franssen

Marco Franssen /

9 min read1781 words

Cover Image for Secure your software supply chain using Sigstore and GitHub actions

With the rise of software supply chain attacks it becomes more important to secure our software supply chains. Many others have been writing about software supply chain attacks already, so I won't repeat that over here in this article. Assuming you found my article, because you want to know how to prevent them.

In this blogpost I want to show you how to secure the software supply chain by applying some SLSA requirements in the GitHub actions workflow. We will utilize Sigstore to sign and attest the images. I will also involve a few other tools to generate build provenance and an SBOM for our released Docker images so we can attest that to the images.

We will cover the following topics:

  • Having a bare bones release workflow using GitHub actions
  • Adding signatures to our Docker images
  • Generating an SBOM for our Docker image
  • Attest the SBOM to our Docker image
  • Generating build provenance for our Docker image
  • Attest the build provenance to our Docker image

⚠️ In this example I only cover releasing a Docker image, but we can apply same to other artifact types, using the same toolchain.

Limit GitHub workflow permissions

Before we get started I first want to point out that you can/should limit the permissions a GitHub action workflow has by default. See following screenshot on how to limit that.

Limit GitHub action workflow permissions

Ensure you pick the Read repository contents permission. This disables write permissions on your repositories by default. For more information on the permissions see permissions for the GitHub token.

You can find these settings at the following URL for your own repositories https://github.com/«your-organization»/«your-repo-goes-here»/settings/actions.

In the remainder of this blog I will show you how to enable specific permissions in your workflows on a per need basis. That way your workflows will have only the bare minimum permissions required for your workflow, reducing the attack surface.

Simple release workflow

First let us have a look at a simple release workflow that builds our Docker images and publishes them to GitHub container registry.

.github/workflows/release.yaml

We require the packages: write permission to be able to push the image to ghcr.io. Once you have executed this workflow a new repository is created with private visibility. To continue with signing using rekor transparancy log you will have to make sure the docker repository is publicly accessible. You can enable this by navigating to your repo. In my case https://github.com/users/marcofranssen/packages/container/slsa-workflow-examples-docker/settings (please note you can't access this URL as you are no admin on this GitHub repository).

Signing our docker images

By adding signatures to our Docker images we make it possible to prove the authenticity of our Docker images using a cryptoghraphically verifyable signature. To add a signature to the Docker image we will add a sign job to the workflow and some additional configuration to capture the image digest for our next steps. To do so we will make use of cosign, rekor and fulcio. Enabling this feature with cosign simply requires setting the following environment variable COSIGN_EXPERIMENTAL=1. We will do this on workflow level by adding this environment variable at workflow level.

.github/workflows/release.yaml
env:
  COSIGN_EXPERIMENTAL: 1
  IMAGE_NAME: ghcr.io/marcofranssen/slsa-workflow-examples-docker

See below the other parts added to the workflow. First take some time to digest these changes, I will explain further below the code block.

.github/workflows/release.yaml
  docker:


    outputs:
      image-digest: ${{ steps.container_info.outputs.image-digest }}

    steps:

      - name: Get container info
        id: container_info
        run: |
          image_digest="$(docker inspect "${IMAGE_NAME}:latest" --format '{{ index .RepoDigests 0 }}' | awk -F '@' '{ print $2 }')"
          echo "::set-output name=image-digest::${image_digest}"

  sign:
    runs-on: ubuntu-20.04
    needs: [docker]

    permissions:
      packages: write
      id-token: write

    env:
      IMAGE_DIGEST: ${{ needs.docker.outputs.image-digest }}

    steps:
      - name: Install cosign
        uses: sigstore/cosign-installer@v2.1.0
        with:
          cosign-release: v1.6.0

      - name: Login to ghcr.io
        uses: docker/login-action@v1.14.1
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Sign image
        run: |
          cosign sign "${IMAGE_NAME}@${IMAGE_DIGEST}"
          echo "::notice title=Verify signature::COSIGN_EXPERIMENTAL=1 cosign verify ${IMAGE_NAME}@${IMAGE_DIGEST} | jq '.[0]'"
          echo "::notice title=Inspect signature bundle::COSIGN_EXPERIMENTAL=1 cosign verify ${IMAGE_NAME}@${IMAGE_DIGEST} | jq '.[0].optional.Bundle.Payload.body |= @base64d | .[0].optional.Bundle.Payload.body | fromjson'"
          echo "::notice title=Inspect certificate::COSIGN_EXPERIMENTAL=1 cosign verify ${IMAGE_NAME}@${IMAGE_DIGEST} | jq -r '.[0].optional.Bundle.Payload.body |= @base64d | .[0].optional.Bundle.Payload.body | fromjson | .spec.signature.publicKey.content |= @base64d | .spec.signature.publicKey.content' | openssl x509 -text"

At the docker job level we configured 1 outputs that allows to pass some information to other jobs. To retrieve this information we add another step in the Docker job that retrieves this information and writes it to the outputs.

Next we add a new job that takes care of signing the Docker image. To be able for this job to authenticate with Fulcio/Rekor we have to enable the id-token permission with write access (remember we disabled all permissions by default on our repositories). Using this experimental feature (at the time of writing this blog) we can enable keyless code-signing by using an OIDC integration between GitHub and Sigstore. The Docker login is required as cosign will store the signature in the OCI registry. See the workflow execution notices on how to verify and inspect the signature of our release.

Before you continue, you workflow should look like this at this stage.

Generate an SBOM

By adding an SBOM we can provide the consumers of our Docker image with an extensive list of our dependencies. At release time our image might not have any known vulnerabilities, but in a couple of weeks/months from now there might be known vulnerabilties with the dependencies in our image. Adding an SBOM allows the consumers of our image to verify if our release contains known vulnerabilities at any future point in time. As well they can inspect the LICENSES involved in our release.

.github/workflows/release.yaml
  sbom:
    runs-on: ubuntu-20.04
    needs: [docker]

    permissions:
      packages: write
      id-token: write

    env:
      IMAGE_DIGEST: ${{ needs.docker.outputs.image-digest }}

    steps:
      - name: Install cosign
        uses: sigstore/cosign-installer@v2.1.0
        with:
          cosign-release: v1.6.0

      - name: Install Syft
        uses: anchore/sbom-action/download-syft@v0.7.0

      - name: Login to ghcr.io
        uses: docker/login-action@v1.14.1
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Attach SBOM to image
        run: |
          syft "${IMAGE_NAME}@${IMAGE_DIGEST}" -o spdx-json=sbom-spdx.json
          cosign attest --predicate sbom-spdx.json --type spdx "${IMAGE_NAME}@${IMAGE_DIGEST}"
          echo "::notice title=Verify SBOM attestation::COSIGN_EXPERIMENTAL=1 cosign verify-attestation ${IMAGE_NAME}@${IMAGE_DIGEST} | jq '.payload |= @base64d | .payload | fromjson | select(.predicateType == \"https://spdx.dev/Document\") | .predicate.Data | fromjson'"

Same story here. To be able to attest the SBOM to our image we need to have the OIDC integration between GitHub and Sigstore, hence the permissions. To generate an SBOM from our Docker image we use syft. The notice logged at the end, gives a nice log message in the workflow run overview so people know how to retrieve and inspect the SBOM for this particular release. See the workflow execution notices

Before you continue you can check here for the current state of our workflow.

Add build provenance

To add build provenance we will make use of slsa-provenance-action. SLSA provenance action allows us to generate build provenance according to the SLSA provenance specification which uses in-toto predicates. in-toto is a framework to secure the integrity of software supply chains.

To be able to store all released tags in the build provenance we need one more output that captures the tags we released.

Add the following to the docker job.

.github/workflows/release.yaml
  docker:

    outputs:
      image-digest: ${{ steps.container_info.outputs.image-digest }}
      image-tags: ${{ steps.container_info.outputs.image-tags }}

    steps:


      - name: Get container info
        id: container_info
        run: |
          image_digest="$(docker inspect "${IMAGE_NAME}:latest" --format '{{ index .RepoDigests 0 }}' | awk -F '@' '{ print $2 }')"
          image_tags="latest,${GITHUB_REF_NAME},$(git rev-parse "${GITHUB_REF_NAME:-HEAD}")"
          echo "::set-output name=image-digest::${image_digest}"
          echo "::set-output name=image-tags::${image_tags}"

Now at the end of the workflow add the following to generate build provenance.

.github/workflows/release.yaml
  provenance:
    runs-on: ubuntu-20.04
    needs: [docker]

    permissions:
      packages: write
      id-token: write

    env:
      IMAGE_DIGEST: ${{ needs.docker.outputs.image-digest }}
      PROVENANCE_FILE: provenance.att

    steps:
      - name: Install cosign
        uses: sigstore/cosign-installer@v2.1.0
        with:
          cosign-release: v1.6.0

      - name: Generate provenance
        uses: philips-labs/slsa-provenance-action@v0.7.2
        with:
          command: generate
          subcommand: container
          arguments: --repository "${IMAGE_NAME}" --output-path "${PROVENANCE_FILE}" --digest "${IMAGE_DIGEST}" --tags "${IMAGE_TAGS}"
        env:
          COSIGN_EXPERIMENTAL: 0
          GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
          IMAGE_TAGS: ${{ needs.docker.outputs.image-tags }}

      - name: Login to ghcr.io
        uses: docker/login-action@v1.14.1
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Attach provenance
        run: |
          jq '.predicate' "${PROVENANCE_FILE}" > provenance-predicate.att
          cosign attest --predicate provenance-predicate.att --type slsaprovenance "${IMAGE_NAME}@${IMAGE_DIGEST}"
          echo "::notice title=Verify provenance attestation::COSIGN_EXPERIMENTAL=1 cosign verify-attestation ${IMAGE_NAME}@${IMAGE_DIGEST} | jq '.payload |= @base64d | .payload | fromjson | select(.predicateType == \"https://slsa.dev/provenance/v0.2\")'"

To be able to attest the provenance to our image we also need the permissions block here for the OIDC integration between GitHub and Sigstore. To generate the provenance we make use of the philips-labs/slsa-provenance-action. This action generates the build provenance based on your GITHUB_CONTEXT and RUNNER_CONTEXT which are environment variables known within a GitHub workflow execution. In the provenance we also capture which Docker tags have been released.

The notice logged at the end, gives a nice log message in the workflow run overview so consumers of the image know how to retrieve and inspect the build provenance for the release.

Summary

The full example can be found here. In the example you will find a small Go app with a single dependency installed to showcase SBOM. See here the end result in action.

With the signature, SBOM and provenance in place, we can now start building policies on our production environments that verify against the signatures and attestations before allowing a deployment.

What other improvements/practices do you suggest to reach a higher SLSA Level? Feel free to contribute to the example, by forking the repository, to help others.

Bonus

Using crane you can easily query the OCI registry to see how the signature and attestations are stored for your image.

terminal
$ crane ls ghcr.io/marcofranssen/slsa-workflow-examples-docker
666a992f55ed7be99945e4019ac9a37d62922543
v0.1.0
8f3a793498eeb0c7eac095d91f87af2846d75a32
v0.1.1
sha256-371450f7c64808fd27f224083c475571e7c0710dc75a536a1755890183234a38.sig
4449252d72ddb46df973fa477309e3ba83a28f07
v0.1.2
sha256-5708dceb1d4e8ed1a7eab0ff7dab6db9742300e713b854334cacd392383be914.sig
sha256-5708dceb1d4e8ed1a7eab0ff7dab6db9742300e713b854334cacd392383be914.att
4d1fcd0b60bf53f4821c8a7ea529e9ea7df70789
latest
v0.1.3
sha256-04b0426d40c824929fd5a75821f39f349db5ffa2eb07df22a55f736d7f40232c.sig
sha256-04b0426d40c824929fd5a75821f39f349db5ffa2eb07df22a55f736d7f40232c.att

References

You have disabled cookies. To leave me a comment please allow cookies at functionality level.

More Stories

Cover Image for OCI as attestations storage for your packages

OCI as attestations storage for your packages

Marco Franssen

Marco Franssen /

In my previous blog you can read about securing the software supply chain for Docker images using GitHub actions and Sigstore. We have seen how we can sign our Docker images, as well how to generate an SBOM and build provenance. Using Sigstore/cosign we attached both the signature, SBOM and build provenance to the Docker image. Using Sigstore we get a real nice integration and developer experience to add these security features to our build pipelines for Docker images. In this blog I want to sh…

Cover Image for Globally configure multiple git commit emails

Globally configure multiple git commit emails

Marco Franssen

Marco Franssen /

Have you ever been struggling to commit with the right email address on different repositories? It happened to me many times in the past, but for a couple of years I'm now using an approach that prevents me from making that mistake. E.g. when working on your work related machine, I'm pretty often also working on Opensource in my spare time, to build my own skills, and simply because I believe in the cause of Opensource. Also during work time I'm also sometimes contributing fixes back to Opensour…

Cover Image for Gitops using Helmsman to apply our Helm Charts to k8s

Gitops using Helmsman to apply our Helm Charts to k8s

Marco Franssen

Marco Franssen /

In my last blog series I have shown an example of deploying Hashicorp Vault on Kubernetes using Helm Charts (see references). This time I want to show you how to more easily integrate this into your … wait for it … 😄, DevSecGitOps flow. Especially Helm charts help a lot in connecting the software part with our infrastructure / deployment (DevOps). Besides that we can embed all kind of security practices in our Helm charts like for example RBAC, Network policies etc. In this blog I want to show…