From 47f80a62c4f0b52a0f8d37c52dba093d2a091b00 Mon Sep 17 00:00:00 2001 From: Martijn Pieters Date: Fri, 31 Jan 2025 15:00:23 +0000 Subject: [PATCH] Sign docker images using cosign (#8685) cosign uses the GitHub action ID token to retrieve an ephemeral code signing certificate from Fulcio, and store the signature in the Rekor transparency log. Once an image has been successfully signed, you should be able to verify the signature with: ```sh cosign verify ghcr.io/astral-sh/uv:latest --certificate-identity-regexp='.*' --certificate-oidc-issuer-regexp='.*' ``` Closes #8670 --- .github/workflows/build-docker.yml | 44 ++++++++++++++++++++++++++++++ .github/workflows/release.yml | 2 ++ Cargo.toml | 2 +- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 843d80e4b..748293412 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -39,6 +39,8 @@ jobs: # Login to DockerHub first, to avoid rate-limiting - uses: docker/login-action@v3 + # PRs from forks don't have access to secrets, disable this step in that case. + if: ${{ github.event.pull_request.head.repo.full_name == 'astral-sh/uv' }} with: username: astralshbot password: ${{ secrets.DOCKERHUB_TOKEN_RO }} @@ -164,6 +166,10 @@ jobs: needs: - docker-publish if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} + permissions: + packages: write + attestations: write # needed to push image attestations to the Github attestation store + id-token: write # needed for signing the images with GitHub OIDC Token strategy: fail-fast: false matrix: @@ -260,6 +266,7 @@ jobs: ${{ env.TAG_PATTERNS }} - name: Build and push + id: build-and-push uses: docker/build-push-action@v6 with: context: . @@ -272,6 +279,13 @@ jobs: labels: ${{ steps.meta.outputs.labels }} annotations: ${{ steps.meta.outputs.annotations }} + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v2 + with: + subject-name: ${{ env.UV_BASE_IMG }} + subject-digest: ${{ steps.build-and-push.outputs.digest }} + # push-to-registry is explicitly not enabled to maintain full control over the top image + # This is effectively a duplicate of `docker-publish` to make https://github.com/astral-sh/uv/pkgs/container/uv # show the uv base image first since GitHub always shows the last updated image digests # This works by annotating the original digests (previously non-annotated) which triggers an update to ghcr.io @@ -283,6 +297,10 @@ jobs: needs: - docker-publish-extra if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} + permissions: + packages: write + attestations: write # needed to push image attestations to the Github attestation store + id-token: write # needed for signing the images with GitHub OIDC Token steps: # Login to DockerHub first, to avoid rate-limiting - uses: docker/login-action@v3 @@ -330,3 +348,29 @@ jobs: "${annotations[@]}" \ $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf '${{ env.UV_BASE_IMG }}@sha256:%s ' *) + + - name: Share manifest digest + id: manifest-digest + # To sign the manifest, we need it's digest. Unfortunately "docker + # buildx imagetools create" does not (yet) have a clean way of sharing + # the digest of the manifest it creates (see docker/buildx#2407), so + # we use a separate command to retrieve it. + # imagetools inspect [TAG] --format '{{json .Manifest}}' gives us + # the machine readable JSON description of the manifest, and the + # jq command extracts the digest from this. The digest is then + # sent to the Github step output file for sharing with other steps. + run: | + digest="$( + docker buildx imagetools inspect \ + "${UV_BASE_IMG}:${DOCKER_METADATA_OUTPUT_VERSION}" \ + --format '{{json .Manifest}}' \ + | jq -r '.digest' + )" + echo "digest=${digest}" >> "$GITHUB_OUTPUT" + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v2 + with: + subject-name: ${{ env.UV_BASE_IMG }} + subject-digest: ${{ steps.manifest-digest.outputs.digest }} + # push-to-registry is explicitly not enabled to maintain full control over the top image diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e2a9ee3d..9de9cbd7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -107,7 +107,9 @@ jobs: plan: ${{ needs.plan.outputs.val }} secrets: inherit permissions: + "attestations": "write" "contents": "read" + "id-token": "write" "packages": "write" # Build and package all the platform-agnostic(ish) things diff --git a/Cargo.toml b/Cargo.toml index 39954fcc0..03b126bc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -335,7 +335,7 @@ publish-jobs = ["./publish-pypi"] # Post-announce jobs to run in CI post-announce-jobs = ["./publish-docs"] # Custom permissions for GitHub Jobs -github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" } } +github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read", id-token = "write", attestations = "write" } } # Whether to install an updater program install-updater = false # Path that installers should place binaries in