Skip to content

Latest commit

 

History

History
666 lines (563 loc) · 28.3 KB

File metadata and controls

666 lines (563 loc) · 28.3 KB

Generation of SLSA3+ provenance for container images

This document explains how to generate SLSA provenance for container images.

This can be done by adding an additional step to your existing Github Actions workflow to call a reusable workflow to generate generic SLSA provenance. We'll call this workflow the "container workflow" from now on.

The container workflow differs from ecosystem specific builders (like the Go builder) which build the artifacts as well as generate provenance. This project simply generates provenance as a separate step in an existing workflow.



Project Status

This workflow is currently under active development. The API could change while approaching an initial release. You can track progress towards General Availability via this milestone.

Benefits of Provenance

Using the container workflow will generate a non-forgeable attestation to the container image using the identity of the GitHub workflow. This can be used to create a positive attestation to a container image coming from your repository.

That means that once your users verify the image they have downloaded they can be sure that the image was created by your repository's workflow and hasn't been tampered with.

Generating Provenance

The container workflow uses a Github Actions reusable workflow to generate the provenance.

Getting Started

To get started, you will need to add some steps to your current workflow. We will assume you have an existing Github Actions workflow to build your project.

provenance:
  needs: [build]
  permissions:
    actions: read # for detecting the Github Actions environment.
    id-token: write # for creating OIDC tokens for signing.
    packages: write # for uploading attestations.
  if: startsWith(github.ref, 'refs/tags/')
  # TODO(https://github.com/slsa-framework/slsa-github-generator/issues/492): Use a tagged release once we have one.
  uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@main
  with:
    image: ${{ needs.build.outputs.tag }}
    digest: ${{ needs.build.outputs.digest }}
    registry-username: ${{ github.actor }}
    # TODO(https://github.com/slsa-framework/slsa-github-generator/issues/492): Remove after GA release.
    compile-generator: true
  secrets:
    registry-password: ${{ secrets.GITHUB_TOKEN }}

Here's an example of what it might look like all together.

env:
  IMAGE_REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # This step builds our image, pushes it, and outputs the repo hash digest.
  build:
    permissions:
      contents: read
      packages: write
    outputs:
      image: ${{ steps.image.outputs.image }}
      digest: ${{ steps.build.outputs.digest }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout the repository
        uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.3.4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@dc7b9719a96d48369863986a06765841d7ea23f6 # v2.0.0

      - name: Authenticate Docker
        uses: docker/login-action@49ed152c8eca782a232dede0303416e8f356c37b # v2.0.0
        with:
          registry: ${{ env.IMAGE_REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@69f6fc9d46f2f8bf0d5491e4aabe0bb8c6a4678a # v4.0.1
        with:
          images: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push Docker image
        uses: docker/build-push-action@e551b19e49efd4e98792db7592c17c09b89db8d8 # v3.0.0
        id: build
        with:
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

      - name: Output image
        id: image
        run: |
          # NOTE: Set the image as an output because the `env` context is not
          # available to the inputs of a reusable workflow call.
          image_name="${IMAGE_REGISTRY}/${IMAGE_NAME}"
          echo "image=$image_name" >> "$GITHUB_OUTPUT"

  # This step calls the container workflow to generate provenance and push it to
  # the container registry.
  provenance:
    needs: [build]
    permissions:
      actions: read # for detecting the Github Actions environment.
      id-token: write # for creating OIDC tokens for signing.
      packages: write # for uploading attestations.
    if: startsWith(github.ref, 'refs/tags/')
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@main
    with:
      image: ${{ needs.build.outputs.image }}
      digest: ${{ needs.build.outputs.digest }}
      registry-username: ${{ github.actor }}
      # TODO(https://github.com/slsa-framework/slsa-github-generator/issues/492): Remove after GA release.
      compile-generator: true
    secrets:
      registry-password: ${{ secrets.GITHUB_TOKEN }}

Referencing the SLSA generator

At present, the generator MUST be referenced by a tag of the form @vX.Y.Z, because the build will fail if you reference it via a shorter tag like @vX.Y or @vX or if you reference it by a hash.

For more information about this design decision and how to configure renovatebot,see the main repository README.md.

Private Repositories

Private repositories are supported with some caveats. Currently all builds generate and post a new entry in the public Rekor API server instance at rekor.sigstore.dev. This entry includes the repository name. This will cause the private repository name to leak and be discoverable via the public Rekor API server.

If this is ok with you, you can set the private-repository flag in order to opt in to publishing to the public Rekor instance from a private repository.

with:
  private-repository: true

If you do not set this flag then private repositories will generate an error in order to prevent leaking repository name information.

Support for private transparency log instances that would not leak repository name information is tracked on issue #372.

Supported Triggers

The following GitHub trigger events are fully supported and tested:

  • schedule
  • push (including new tags)
  • release
  • Manual run via workflow_dispatch

However, in practice, most triggers should work with the exception of pull_request. If you would like support for pull_request, please tell us about your use case on issue #358. If you have an issue in all other triggers please submit a new issue.

Workflow Inputs

The container workflow accepts the following inputs:

Inputs:

Name Required Default Description
image yes The OCI image name. This must not include a tag or digest.
digest yes The OCI image digest. The image digest of the form ':' (e.g. 'sha256:abcdef...')
registry-username yes Username to log into the container registry.
compile-generator false false Whether to build the generator from source. This increases build time by ~2m.
private-repository no false Set to true to opt-in to posting to the public transparency log. Will generate an error if false for private repositories. This input has no effect for public repositories. See Private Repositories.

Secrets:

Name Required Description
registry-password yes Password to log in the container registry.

Provenance Format

The project generates SLSA provenance with the following values.

Name Value Description
buildType "https://github.com/slsa-framework/slsa-github-generator/container@v1" Identifies a the GitHub Actions build.
metadata.buildInvocationID "[run_id]-[run_attempt]" The GitHub Actions run_id does not update when a workflow is re-run. Run attempt is added to make the build invocation ID unique.

Provenance Example

The following is an example of the generated proveanance. Provenance is generated as an in-toto statement with a SLSA predicate.

{
  "_type": "https://in-toto.io/Statement/v0.1",
  "predicateType": "https://slsa.dev/provenance/v0.2",
  "subject": [
    {
      "name": "ghcr.io/ianlewis/actions-test",
      "digest": {
        "sha256": "8ae83e5b11e4cc8257f5f4d1023081ba1c72e8e60e8ed6cacd0d53a4ca2d142b"
      }
    },
  ],
  "predicate": {
    "builder": {
      "id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v1.1.1"
    },
    "buildType": "https://github.com/slsa-framework/slsa-github-generator/container@v1",
    "invocation": {
      "configSource": {
        "uri": "git+https://github.com/ianlewis/actions-test@refs/heads/main.git",
        "digest": {
          "sha1": "e491e4b2ce5bc76fb103729b61b04d3c46d8a192"
        },
        "entryPoint": ".github/workflows/generic-container.yml"
      },
      "parameters": {},
      "environment": {
        "github_actor": "ianlewis",
        "github_actor_id": "49289",
        "github_base_ref": "",
        "github_event_name": "push",
        "github_event_payload": {...},
        "github_head_ref": "",
        "github_ref": "refs/tags/v0.0.9",
        "github_ref_type": "tag",
        "github_repository_id": "474793590",
        "github_repository_owner": "ianlewis",
        "github_repository_owner_id": "49289",
        "github_run_attempt": "1",
        "github_run_id": "2556669934",
        "github_run_number": "12",
        "github_sha1": "e491e4b2ce5bc76fb103729b61b04d3c46d8a192"
      }
    },
    "metadata": {
      "buildInvocationID": "2556669934-1",
      "completeness": {
        "parameters": true,
        "environment": false,
        "materials": false
      },
      "reproducible": false
    },
    "materials": [
      {
        "uri": "git+https://github.com/ianlewis/actions-test@refs/tags/v0.0.9",
        "digest": {
          "sha1": "e491e4b2ce5bc76fb103729b61b04d3c46d8a192"
        }
      }
    ]
  }
}

Integration With Other Build Systems

This section explains how to generate non-forgeable SLSA provenance with existing build systems.

Ko

ko is a simple, fast container image builder for Go applications. If you want to use ko you can generate SLSA3 provenance by updating your workflow withe following steps:

  1. Declare an outputs for the build job:
jobs:
  build:
    outputs:
      image: ${{ steps.build.outputs.image }}
      digest: ${{ steps.build.outputs.digest }}
  1. Add an id: build field to your ko step. Update the step to output the image and digest.
steps:
  [...]
  - name: Run ko
    id: build
    env:
      KO_DOCKER_REPO: "${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}"
      KO_USER: ${{ github.actor }}
      KO_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
      GIT_REF: ${{ github.ref }}
    run: |
      # get tag name without tags/refs/ prefix.
      tag=$(echo ${GIT_REF} | cut -d'/' -f3)

      # Log into regisry
      echo "${KO_PASSWORD}" | ko login ghcr.io --username "$KO_USER" --password-stdin

      # Build & push the image. Save the image name.
      image_and_digest=$(ko build --tags="${tag}" .)

      # Output the image name and digest so we can generate provenance.
      image=$(echo "${image_and_digest}" | cut -d':' -f1)
      digest=$(echo "${image_and_digest}" | cut -d'@' -f2)
      echo "::set-output name=image::$image"
      echo "::set-output name=digest::$digest"
  1. Call the generic container workflow to generate provenance by declaring the job below:
provenance:
  needs: [build]
  permissions:
    actions: read
    id-token: write
    # contents: read
    packages: write
  if: startsWith(github.ref, 'refs/tags/')
  # TODO: Update after GA
  # uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
  uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@9dc6318aedc3d24ede4e946966d30c752769a4f9
  with:
    image: ${{ needs.build.outputs.image }}
    digest: ${{ needs.build.outputs.digest }}
    registry-username: ${{ github.actor }}
    compile-generator: true
  secrets:
    registry-password: ${{ secrets.GITHUB_TOKEN }}

All together, it will look as the following:

jobs:
  build:
    permissions:
      contents: read
      packages: write
    outputs:
      image: ${{ steps.build.outputs.image }}
      digest: ${{ steps.build.outputs.digest }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout the repository
        uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2.3.4

      - uses: actions/[email protected]
        with:
          go-version: 1.19

      - name: Set up ko
        uses: imjasonh/[email protected]

      - name: Run ko
        id: build
        env:
          KO_DOCKER_REPO: "${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}"
          KO_USER: ${{ github.actor }}
          KO_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
          GIT_REF: ${{ github.ref }}
        run: |
          # get tag name without tags/refs/ prefix.
          tag=$(echo ${GIT_REF} | cut -d'/' -f3)

          # Log into regisry
          echo "${KO_PASSWORD}" | ko login ghcr.io --username "$KO_USER" --password-stdin

          # Build & push the image. Save the image name.
          image_and_digest=$(ko build --tags="${tag}" .)

          # Output the image name and digest so we can generate provenance.
          image=$(echo "${image_and_digest}" | cut -d':' -f1)
          digest=$(echo "${image_and_digest}" | cut -d'@' -f2)
          echo "::set-output name=image::$image"
          echo "::set-output name=digest::$digest"

  # This step calls the generic workflow to generate provenance.
  provenance:
    needs: [build]
    permissions:
      actions: read
      id-token: write
      # contents: read
      packages: write
    if: startsWith(github.ref, 'refs/tags/')
    # uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@9dc6318aedc3d24ede4e946966d30c752769a4f9
    with:
      image: ${{ needs.build.outputs.image }}
      digest: ${{ needs.build.outputs.digest }}
      registry-username: ${{ github.actor }}
      compile-generator: true
    secrets:
      registry-password: ${{ secrets.GITHUB_TOKEN }}

Verification

Verification of provenance attestations can be done via several different tools. This section shows examples of several popular tools.

Cosign

Cosign can be used to verify the provenance attestation for the image. A CUE policy can also be used to verify parts of the SLSA attestation.

Here is an example policy stored in policy.cue:

// The predicateType field must match this string
predicateType: "https://slsa.dev/provenance/v0.2"

predicate: {
  // This condition verifies that the builder is the builder we
  // expect and trust. The following condition can be used
  // unmodified. It verifies that the builder is the container
  // workflow.
  builder: {
    // TODO: update after GA
    // id: =~"^https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v[0-9]+.[0-9]+.[0-9]+$"
    id: =~"^https://github.com/ianlewis/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/heads/409-feature-add-generic-container-workflow$"
  }
  invocation: {
    configSource: {
      // This condition verifies the entrypoint of the workflow.
      // Replace with the relative path to your workflow in your
      // repository.
      entryPoint: ".github/workflows/generic-container.yml"

      // This condition verifies that the image was generated from
      // the source repository we expect. Replace this with your
      // repository.
      uri: =~"^git\\+https://github.com/ianlewis/actions-test@refs/tags/v[0-9]+.[0-9]+.[0-9]+$"
    }
  }
}

We can then use cosign to verify the attestation using the policy.

$ COSIGN_EXPERIMENTAL=1 cosign verify-attestation \
  --type slsaprovenance \
  --policy policy.cue \
  ghcr.io/ianlewis/actions-test:v0.0.38 > /dev/null
will be validating against CUE policies: [policy.cue]

Verification for ghcr.io/ianlewis/actions-test:v0.0.38 --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - Existence of the claims in the transparency log was verified offline
  - Any certificates were verified against the Fulcio roots.
Certificate subject:  https://github.com/ianlewis/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/heads/409-feature-add-generic-container-workflow
Certificate issuer URL:  https://token.actions.githubusercontent.com

You can read more in the cosign documentation.

Sigstore policy-controller

Sigstore policy-controller is a policy management controller that can be used to write Kubernetes-native policies for SLSA provenance. The following assumes you have installed policy-controller in your Kubernetes cluster.

The following ClusterImagePolicy can be used to verify container images as part of Admission Control.

apiVersion: policy.sigstore.dev/v1alpha1
kind: ClusterImagePolicy
metadata:
  name: image-is-signed-by-github-actions
spec:
  images:
    # Matches all versions of the actions-test image.
    # NOTE: policy-controller mutates pods to use a digest even if originally
    # specified by tag.
    - glob: "ghcr.io/ianlewis/actions-test@*"
  authorities:
    - keyless:
        # Signed by the public Fulcio certificate authority
        url: https://fulcio.sigstore.dev
        identities:
          # Matches the Github Actions OIDC issuer
          - issuer: https://token.actions.githubusercontent.com
            # Matches the reusable workflow's signing identity.
            # TODO: update after GA
            subjectRegExp: "^https://github.com/ianlewis/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/heads/409-feature-add-generic-container-workflow$"
      attestations:
        - name: must-have-slsa
          predicateType: slsaprovenance
          policy:
            type: cue
            data: |
              // The predicateType field must match this string
              predicateType: "https://slsa.dev/provenance/v0.2"

              predicate: {
                // This condition verifies that the builder is the builder we
                // expect and trust. The following condition can be used
                // unmodified. It verifies that the builder is the container
                // workflow.
                builder: {
                  // TODO: update after GA
                  // id: =~"^https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v[0-9]+.[0-9]+.[0-9]+$"
                  id: =~"^https://github.com/ianlewis/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/heads/409-feature-add-generic-container-workflow$"
                }
                invocation: {
                  configSource: {
                    // This condition verifies the entrypoint of the workflow.
                    // Replace with the relative path to your workflow in your
                    // repository.
                    entryPoint: ".github/workflows/generic-container.yml"

                    // This condition verifies that the image was generated from
                    // the source repository we expect. Replace this with your
                    // repository.
                    uri: =~"^git\\+https://github.com/ianlewis/actions-test@refs/tags/v[0-9]+.[0-9]+.[0-9]+$"
                  }
                }
              }

When applied the ClusterImagePolicy will be evaluated when a Pod is created. If the Pod fulfills the policy conditions then the Pod can be created. If the Pod does not fulfill one or more of the policy conditions then you will see an error like the following. For example, the following error will occur when issuer does not match.

$ kubectl run actions-test --image=ghcr.io/ianlewis/actions-test:v0.0.38 --port=8080
Error from server (BadRequest): admission webhook "policy.sigstore.dev" denied the request: validation failed: failed policy: image-is-signed-by-github-actions: spec.containers[0].image
ghcr.io/ianlewis/actions-test@sha256:7c01e1c050f6b7a9b38a53da1be0835288da538d506de571f654417ae89aea4e attestation keyless validation failed for authority authority-0 for ghcr.io/ianlewis/actions-test@sha256:7c01e1c050f6b7a9b38a53da1be0835288da538d506de571f654417ae89aea4e: no matching attestations:
none of the expected identities matched what was in the certificate

This behavior can be configured to allow, deny, or warn depending on your use case. See the sigstore docs for more info.

Kyverno

Kyverno is a policy management controller that can be used to write Kubernetes-native policies for SLSA provenance. The following assumes you have installed Kyverno in your Kubernetes cluster.

The following Kyverno ClusterPolicy can be used to verify container images as part of Admission Control.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: check-slsa-attestations
spec:
  validationFailureAction: enforce
  webhookTimeoutSeconds: 30
  rules:
    - name: check-all-keyless
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        # imageReferences sets which images the policy will apply to.
        # Replace with your image. Wildcard values are supported.
        - imageReferences:
            - "ghcr.io/ianlewis/actions-test:*"
          attestors:
            # This section declares which attestors are accepted. The subject
            # below corresponds to the OIDC identity of the container workflow.
            # The issuer corresponds to the GitHub OIDC server that issues the
            # identity.
            - entries:
                - keyless:
                    subject: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v1.2.0"
                    issuer: "https://token.actions.githubusercontent.com"
          # This section declares some policy conditions acting on the provenance itself.
          attestations:
            - predicateType: https://slsa.dev/provenance/v0.2
              conditions:
                - all:
                    # This condition verifies that the image was generated from
                    # the source repository we expect. Replace this with your
                    # repository.
                    - key: "{{ invocation.configSource.uri }}"
                      operator: Equals
                      value: "git+https://github.com/ianlewis/actions-test@refs/tags/v0.0.11"

                    # This condition verifies the entrypoint of the workflow.
                    # Replace with the relative path to your workflow in your
                    #  repository.
                    - key: "{{ invocation.configSource.entryPoint }}"
                      operator: Equals
                      value: ".github/workflows/generic-container.yaml"

                    # This condition verifies that the builder is the builder we
                    # expect and trust. The following condition can be used
                    # unmodified. It verifies that the builder is the container
                    # workflow.
                    - key: "{{ regex_match('^https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v[0-9].[0-9].[0-9]$','{{ builder.id}}') }}"
                      operator: Equals
                      value: true

When applied the ClusterPolicy will be evaluated when a Pod is created. If the Pod fulfills the policy conditions then the Pod can be created. If the Pod does not fulfill one or more of the policy conditions then you will see an error like the following. For example, the following error will occur when no attestation for the image can be found.

$ kubectl apply -f pod.yaml
Error from server: error when creating "pod.yaml": admission webhook "mutate.kyverno.svc-fail" denied the request:

resource Pod/default/actions-test was blocked due to the following policies

check-slsa-attestations:
  check-all-keyless: |-
    failed to verify signature for ghcr.io/ianlewis/actions-test:v0.0.11: .attestors[0].entries[0].keyless: no matching attestations:
    no certificate found on attestation