From b9935137237ad1fc401477244f0a7833626b94de Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Mon, 17 Aug 2026 11:55:57 +1000 Subject: [PATCH 1/9] UID2-7080: Add SLSA provenance for private operator artifacts Gate non-snapshot private image tags and release files on attest+verify so enclave images, measurements, and deployment archives publish with the same producer-side provenance controls as the public operator image. Co-authored-by: Cursor --- .github/actions/attest_file/action.yaml | 43 +++++ .github/actions/build_ami/action.yaml | 47 ++++-- .../build_eks_docker_image/action.yaml | 45 +++++- .../verify_oci_attestation/action.yaml | 36 +++++ .github/workflows/build-uid2-ami.yaml | 32 ++++ .github/workflows/publish-all-operators.yaml | 17 ++ .../publish-aws-eks-nitro-enclave-docker.yaml | 22 +++ .github/workflows/publish-aws-nitro-eif.yaml | 62 +++++++ .../publish-azure-cc-enclave-docker.yaml | 69 +++++++- .../publish-gcp-oidc-enclave-docker.yaml | 153 +++++++++++++++--- README.md | 102 ++++++++++-- 11 files changed, 575 insertions(+), 53 deletions(-) create mode 100644 .github/actions/attest_file/action.yaml create mode 100644 .github/actions/verify_oci_attestation/action.yaml diff --git a/.github/actions/attest_file/action.yaml b/.github/actions/attest_file/action.yaml new file mode 100644 index 000000000..f39b8c0c1 --- /dev/null +++ b/.github/actions/attest_file/action.yaml @@ -0,0 +1,43 @@ +name: Attest files +description: Generates and verifies SLSA build provenance for one or more files. + +inputs: + subject_paths: + description: Newline-separated paths to the files to attest. + required: true + +runs: + using: composite + steps: + - name: Attest build provenance + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + with: + subject-path: ${{ inputs.subject_paths }} + + - name: Verify attestations + shell: bash + env: + GH_TOKEN: ${{ github.token }} + SUBJECT_PATHS: ${{ inputs.subject_paths }} + WORKFLOW_REF: ${{ github.workflow_ref }} + run: | + set -euo pipefail + + # Reusable workflow_call jobs are signed as the caller + # (publish-all-operators). Standalone dispatches are signed as this + # workflow file. Reject every other workflow in the repository. + signer_file="$(basename "${WORKFLOW_REF%%@*}")" + cert_identity_regex="^https://github\\.com/${GITHUB_REPOSITORY}/\\.github/workflows/(publish-all-operators\\.ya?ml|${signer_file})@" + + while IFS= read -r subject_path; do + [[ -z "$subject_path" ]] && continue + + if [[ ! -f "$subject_path" ]]; then + echo "::error file=${subject_path}::Attestation subject does not exist" + exit 1 + fi + + gh attestation verify "$subject_path" \ + --repo "$GITHUB_REPOSITORY" \ + --cert-identity-regex "$cert_identity_regex" + done <<< "$SUBJECT_PATHS" diff --git a/.github/actions/build_ami/action.yaml b/.github/actions/build_ami/action.yaml index 70071c8f0..77d4eaee8 100644 --- a/.github/actions/build_ami/action.yaml +++ b/.github/actions/build_ami/action.yaml @@ -42,6 +42,9 @@ outputs: enclave_id: description: the Enclave_ID of the EIF used to create the AMI value: ${{ steps.extractAmiIdAndEnclaveId.outputs.ENCLAVE_ID }} + measurement_path: + description: Path to the generated AMI measurement metadata file + value: ${{ steps.measurementPath.outputs.path }} runs: using: "composite" @@ -87,10 +90,6 @@ runs: FILE=$(echo $ARTIFACTS | jq -r '.[0].name') unzip -o -d ./scripts/aws/uid2-operator-ami/artifacts $FILE.zip rm $FILE.zip - cd "./scripts/aws/uid2-operator-ami/artifacts/" - zip "uid2operatoreif.zip" "uid2operator.eif" - cd - - rm ./scripts/aws/uid2-operator-ami/artifacts/uid2operator.eif ls ./scripts/aws/uid2-operator-ami/artifacts/ -al - name: Configure UID2 AWS credentials @@ -122,6 +121,34 @@ runs: echo "VERSION_NUMBER=$VERSION_NUMBER" >> $GITHUB_OUTPUT echo $VERSION_NUMBER + - name: Verify consumed EIF provenance + if: ${{ !contains(steps.versionNumber.outputs.VERSION_NUMBER, 'SNAPSHOT') }} + shell: bash + env: + GH_TOKEN: ${{ inputs.github_token }} + EIF_PATH: ${{ github.workspace }}/scripts/aws/uid2-operator-ami/artifacts/uid2operator.eif + EIF_REPO: ${{ inputs.eif_repo_owner }}/${{ inputs.eif_repo_name }} + run: | + set -euo pipefail + if [[ ! -f "$EIF_PATH" ]]; then + echo "::error file=${EIF_PATH}::Expected EIF subject is missing before AMI build" + exit 1 + fi + # EIF may have been attested by the standalone Nitro workflow or by + # Publish All Operators calling that workflow via workflow_call. + gh attestation verify "$EIF_PATH" \ + --repo "$EIF_REPO" \ + --cert-identity-regex "^https://github\\.com/${EIF_REPO}/\\.github/workflows/(publish-aws-nitro-eif|publish-all-operators)\\.ya?ml@" + + - name: Prepare EIF archive for Packer + shell: bash + run: | + set -euo pipefail + cd "./scripts/aws/uid2-operator-ami/artifacts/" + zip "uid2operatoreif.zip" "uid2operator.eif" + rm "uid2operator.eif" + ls -al + - name: Setup Packer id: setup-packer uses: hashicorp/setup-packer@3286471d6cc6756d056a0b199fea5e0becdbc189 # v3.3.0 @@ -189,8 +216,10 @@ runs: cat euid_AMI_measurement.txt ls -al - - name: Upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ inputs.identity_scope }}_AMI_measurement - path: ./scripts/aws/uid2-operator-ami/${{ inputs.identity_scope }}_AMI_measurement.txt + - name: Set measurement metadata path + id: measurementPath + shell: bash + env: + IDENTITY_SCOPE: ${{ inputs.identity_scope }} + run: | + echo "path=${GITHUB_WORKSPACE}/scripts/aws/uid2-operator-ami/${IDENTITY_SCOPE}_AMI_measurement.txt" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/build_eks_docker_image/action.yaml b/.github/actions/build_eks_docker_image/action.yaml index 596fad262..ae6143e6e 100644 --- a/.github/actions/build_eks_docker_image/action.yaml +++ b/.github/actions/build_eks_docker_image/action.yaml @@ -111,6 +111,9 @@ runs: run: | df -h + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - name: Log in to the Docker container registry uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: @@ -126,6 +129,15 @@ runs: tags: | type=raw,value=${{ steps.versionNumber.outputs.VERSION_NUMBER }}.${{ github.run_number }} + - name: Set image repository reference + id: image-ref + shell: bash + env: + IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-eks-${{ inputs.identity_scope }} + run: | + image="$(printf '%s' "$IMAGE" | tr '[:upper:]' '[:lower:]')" + echo "value=${image}" >> "$GITHUB_OUTPUT" + - name: Build and export to Docker uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: @@ -138,17 +150,44 @@ runs: IMAGE_VERSION=${{ steps.versionNumber.outputs.VERSION_NUMBER }} BUILD_TARGET=${{ env.ENCLAVE_PROTOCOL }} - - name: Push to Docker + - name: Push to Docker by digest id: push-to-docker uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: ${{ inputs.artifacts_output_dir }} - push: true - tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ steps.image-ref.outputs.value }},push-by-digest=true,push=true build-args: | JAR_VERSION=${{ steps.versionNumber.outputs.VERSION_NUMBER }} IMAGE_VERSION=${{ steps.versionNumber.outputs.VERSION_NUMBER }} + BUILD_TARGET=${{ env.ENCLAVE_PROTOCOL }} + + - name: Attest image + if: ${{ !contains(steps.versionNumber.outputs.VERSION_NUMBER, 'SNAPSHOT') }} + uses: IABTechLab/uid2-shared-actions/actions/attest_image@v3 + with: + subject_name: ${{ steps.image-ref.outputs.value }} + subject_digest: ${{ steps.push-to-docker.outputs.digest }} + + - name: Verify image attestation from registry + if: ${{ !contains(steps.versionNumber.outputs.VERSION_NUMBER, 'SNAPSHOT') }} + uses: ./.github/actions/verify_oci_attestation + with: + subject_name: ${{ steps.image-ref.outputs.value }} + subject_digest: ${{ steps.push-to-docker.outputs.digest }} + + - name: Promote digest to Docker tags + shell: bash + env: + DIGEST: ${{ steps.push-to-docker.outputs.digest }} + IMAGE: ${{ steps.image-ref.outputs.value }} + TAGS: ${{ steps.meta.outputs.tags }} + run: | + set -euo pipefail + while IFS= read -r tag; do + [[ -z "$tag" ]] && continue + docker buildx imagetools create -t "$tag" "${IMAGE}@${DIGEST}" + done <<< "$TAGS" - name: Check disk usage shell: bash diff --git a/.github/actions/verify_oci_attestation/action.yaml b/.github/actions/verify_oci_attestation/action.yaml new file mode 100644 index 000000000..5282efddd --- /dev/null +++ b/.github/actions/verify_oci_attestation/action.yaml @@ -0,0 +1,36 @@ +name: Verify OCI attestation +description: | + Verifies that a registry-stored SLSA attestation bundle is retrievable for an + OCI image digest and was signed by this workflow or by Publish All Operators + when that workflow is the caller. + +inputs: + subject_name: + description: Fully qualified image reference (registry/owner/repo[+suffix]). + required: true + subject_digest: + description: OCI manifest digest (sha256:...) emitted by docker/build-push-action. + required: true + +runs: + using: composite + steps: + - name: Verify attestation from registry + shell: bash + env: + GH_TOKEN: ${{ github.token }} + SUBJECT_NAME: ${{ inputs.subject_name }} + SUBJECT_DIGEST: ${{ inputs.subject_digest }} + WORKFLOW_REF: ${{ github.workflow_ref }} + run: | + set -euo pipefail + + image_ref="$(printf '%s' "$SUBJECT_NAME" | tr '[:upper:]' '[:lower:]')" + signer_file="$(basename "${WORKFLOW_REF%%@*}")" + cert_identity_regex="^https://github\\.com/${GITHUB_REPOSITORY}/\\.github/workflows/(publish-all-operators\\.ya?ml|${signer_file})@" + + gh attestation verify \ + "oci://${image_ref}@${SUBJECT_DIGEST}" \ + --bundle-from-oci \ + --repo "$GITHUB_REPOSITORY" \ + --cert-identity-regex "$cert_identity_regex" diff --git a/.github/workflows/build-uid2-ami.yaml b/.github/workflows/build-uid2-ami.yaml index cb03caf8b..ee338f7c1 100644 --- a/.github/workflows/build-uid2-ami.yaml +++ b/.github/workflows/build-uid2-ami.yaml @@ -31,7 +31,10 @@ jobs: name: UID2 Operator AMI runs-on: ubuntu-latest permissions: + contents: read id-token: write + attestations: write + artifact-metadata: write outputs: version_number: ${{ steps.buildAMI.outputs.version_number }} ami_id: ${{ steps.buildAMI.outputs.ami_id }} @@ -55,6 +58,19 @@ jobs: euid_aws_role: ${{ env.EUID_AWS_ROLE }} euid_aws_region: ${{ env.EUID_AWS_REGION }} + - name: Attest UID2 AMI metadata + if: ${{ !contains(steps.buildAMI.outputs.version_number, 'SNAPSHOT') }} + uses: ./.github/actions/attest_file + with: + subject_paths: ${{ steps.buildAMI.outputs.measurement_path }} + + - name: Upload UID2 AMI metadata + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: uid2_AMI_measurement + path: ${{ steps.buildAMI.outputs.measurement_path }} + if-no-files-found: error + testUID2Ami: name: E2E Tests UID2 AMI uses: ./.github/workflows/run-e2e-tests-on-operator.yaml @@ -71,7 +87,10 @@ jobs: name: EUID Operator AMI runs-on: ubuntu-latest permissions: + contents: read id-token: write + attestations: write + artifact-metadata: write outputs: version_number: ${{ steps.buildAMI.outputs.version_number }} ami_id: ${{ steps.buildAMI.outputs.ami_id }} @@ -105,6 +124,19 @@ jobs: euid_aws_role: ${{ env.EUID_AWS_ROLE }} euid_aws_region: ${{ env.EUID_AWS_REGION }} + - name: Attest EUID AMI metadata + if: ${{ !contains(steps.buildAMI.outputs.version_number, 'SNAPSHOT') }} + uses: ./.github/actions/attest_file + with: + subject_paths: ${{ steps.buildAMI.outputs.measurement_path }} + + - name: Upload EUID AMI metadata + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: euid_AMI_measurement + path: ${{ steps.buildAMI.outputs.measurement_path }} + if-no-files-found: error + testEUIDAmi: name: E2E Tests EUID AMI uses: ./.github/workflows/run-e2e-tests-on-operator.yaml diff --git a/.github/workflows/publish-all-operators.yaml b/.github/workflows/publish-all-operators.yaml index cf938b3b0..0618d9f53 100644 --- a/.github/workflows/publish-all-operators.yaml +++ b/.github/workflows/publish-all-operators.yaml @@ -159,6 +159,11 @@ jobs: createRelease: name: Create Release runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + attestations: write + artifact-metadata: write # Also publish on scheduled builds, not just manual dispatch: the deployed # operator versions may come from the daily scheduled run, so # the uid2-deployment pre-deploy release gate won't hard-block @@ -241,6 +246,18 @@ jobs: (cd ./deployment/gcp-oidc-deployment-files-${{ needs.start.outputs.new_version }} && zip -r ../../gcp-oidc-deployment-files-${{ needs.start.outputs.new_version }}.zip . ) (cd manifests && zip -r ../uid2-operator-release-manifests-${{ needs.start.outputs.new_version }}.zip .) + - name: Attest release archives + if: ${{ !contains(needs.start.outputs.new_version, 'SNAPSHOT') }} + uses: ./.github/actions/attest_file + with: + subject_paths: | + ./aws-euid-deployment-files-${{ needs.start.outputs.new_version }}.zip + ./aws-uid2-deployment-files-${{ needs.start.outputs.new_version }}.zip + ./azure-cc-deployment-files-${{ needs.start.outputs.new_version }}.zip + ./azure-aks-deployment-files-${{ needs.start.outputs.new_version }}.zip + ./gcp-oidc-deployment-files-${{ needs.start.outputs.new_version }}.zip + ./uid2-operator-release-manifests-${{ needs.start.outputs.new_version }}.zip + # Publish as a pre-release (not a draft): durable + fetchable by tag # without claiming GA. The Major-release approval gate stays the # check_major job above, and promoting this to Latest in the UI remains diff --git a/.github/workflows/publish-aws-eks-nitro-enclave-docker.yaml b/.github/workflows/publish-aws-eks-nitro-enclave-docker.yaml index f2f08cc56..3bef3ab5e 100644 --- a/.github/workflows/publish-aws-eks-nitro-enclave-docker.yaml +++ b/.github/workflows/publish-aws-eks-nitro-enclave-docker.yaml @@ -35,6 +35,9 @@ jobs: contents: write security-events: write packages: write + id-token: write + attestations: write + artifact-metadata: write steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -63,6 +66,9 @@ jobs: contents: write security-events: write packages: write + id-token: write + attestations: write + artifact-metadata: write steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -110,7 +116,15 @@ jobs: name: Cleanup Building AWS Image runs-on: ubuntu-latest needs: [buildUID2Image, buildEUIDImage, testUID2Eks, testEUIDEks] + permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Check disk usage shell: bash run: | @@ -127,6 +141,14 @@ jobs: echo "EUID Image Tag: ${{ needs.buildEUIDImage.outputs.image_tag }}" >> ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/manifests/aws-eks-euid-enclave-id-${{ needs.buildEUIDImage.outputs.image_tag }}.txt echo "Enclave ID (maybe shared by other images): " ${{ needs.buildEUIDImage.outputs.enclave_id }} >> ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/manifests/aws-eks-euid-enclave-id-${{ needs.buildEUIDImage.outputs.image_tag }}.txt + - name: Attest enclave ID manifests + if: ${{ !contains(needs.buildUID2Image.outputs.eif_version_number, 'SNAPSHOT') }} + uses: ./.github/actions/attest_file + with: + subject_paths: | + ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/manifests/aws-eks-uid2-enclave-id-${{ needs.buildUID2Image.outputs.image_tag }}.txt + ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/manifests/aws-eks-euid-enclave-id-${{ needs.buildEUIDImage.outputs.image_tag }}.txt + - name: Save Manifests as build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.github/workflows/publish-aws-nitro-eif.yaml b/.github/workflows/publish-aws-nitro-eif.yaml index 3df81afe9..463786f3d 100644 --- a/.github/workflows/publish-aws-nitro-eif.yaml +++ b/.github/workflows/publish-aws-nitro-eif.yaml @@ -72,6 +72,11 @@ jobs: name: Build UID2 EIF runs-on: ubuntu-latest needs: start + permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -92,6 +97,14 @@ jobs: run: | df -h + - name: Attest UID2 EIF files + if: ${{ !contains(needs.start.outputs.new_version, 'SNAPSHOT') }} + uses: ./.github/actions/attest_file + with: + subject_paths: | + ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/uid2/uid2operator.eif + ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/uid2/pcr0.txt + - name: Save UID2 eif artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -111,6 +124,11 @@ jobs: name: Build EUID EIF runs-on: ubuntu-latest needs: start + permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -131,6 +149,14 @@ jobs: run: | df -h + - name: Attest EUID EIF files + if: ${{ !contains(needs.start.outputs.new_version, 'SNAPSHOT') }} + uses: ./.github/actions/attest_file + with: + subject_paths: | + ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/euid/uid2operator.eif + ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/euid/pcr0.txt + - name: Save EUID eif artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -150,7 +176,15 @@ jobs: name: Cleanup Building AWS Image runs-on: ubuntu-latest needs: [start, buildUID2EIF, buildEUIDEIF] + permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Check disk usage shell: bash run: | @@ -159,11 +193,13 @@ jobs: - name: Download UID2 artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: + name: aws-uid2-deployment-files-${{ needs.start.outputs.new_version }} path: ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/uid2 - name: Download EUID artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: + name: aws-euid-deployment-files-${{ needs.start.outputs.new_version }} path: ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/euid - name: Save Enclave Ids @@ -172,6 +208,14 @@ jobs: echo ${{ needs.buildUID2EIF.outputs.uid2_enclave_id }} >> ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/manifests/aws-uid2-enclave-id-${{ needs.start.outputs.new_version }}.txt echo ${{ needs.buildEUIDEIF.outputs.euid_enclave_id }} >> ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/manifests/aws-euid-enclave-id-${{ needs.start.outputs.new_version }}.txt + - name: Attest enclave ID manifests + if: ${{ !contains(needs.start.outputs.new_version, 'SNAPSHOT') }} + uses: ./.github/actions/attest_file + with: + subject_paths: | + ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/manifests/aws-uid2-enclave-id-${{ needs.start.outputs.new_version }}.txt + ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/manifests/aws-euid-enclave-id-${{ needs.start.outputs.new_version }}.txt + - name: Save Manifests as build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -192,6 +236,24 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Zip deployment artifacts + if: ${{ inputs.version_number_input == '' && needs.start.outputs.is_release == 'true' }} + env: + VERSION: ${{ needs.start.outputs.new_version }} + ARTIFACTS_DIR: ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }} + run: | + set -euo pipefail + (cd "${ARTIFACTS_DIR}/uid2" && zip -r "${ARTIFACTS_DIR}/aws-uid2-deployment-artifacts-${VERSION}.zip" .) + (cd "${ARTIFACTS_DIR}/euid" && zip -r "${ARTIFACTS_DIR}/aws-euid-deployment-artifacts-${VERSION}.zip" .) + + - name: Attest deployment archives + if: ${{ inputs.version_number_input == '' && needs.start.outputs.is_release == 'true' && !contains(needs.start.outputs.new_version, 'SNAPSHOT') }} + uses: ./.github/actions/attest_file + with: + subject_paths: | + ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/aws-uid2-deployment-artifacts-${{ needs.start.outputs.new_version }}.zip + ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/aws-euid-deployment-artifacts-${{ needs.start.outputs.new_version }}.zip + - name: Create release if: ${{ inputs.version_number_input == '' && needs.start.outputs.is_release == 'true' }} uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 diff --git a/.github/workflows/publish-azure-cc-enclave-docker.yaml b/.github/workflows/publish-azure-cc-enclave-docker.yaml index 81346d6cf..026e871e8 100644 --- a/.github/workflows/publish-azure-cc-enclave-docker.yaml +++ b/.github/workflows/publish-azure-cc-enclave-docker.yaml @@ -66,6 +66,8 @@ jobs: security-events: write packages: write id-token: write + attestations: write + artifact-metadata: write pull-requests: write outputs: jar_version: ${{ steps.update_version.outputs.new_version }} @@ -102,6 +104,9 @@ jobs: cp -r target ${{ env.DOCKER_CONTEXT_PATH }}/ cp scripts/confidential_compute.py ${{ env.DOCKER_CONTEXT_PATH }}/ + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - name: Log in to the Docker container registry uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: @@ -117,6 +122,14 @@ jobs: tags: | type=raw,value=${{ steps.update_version.outputs.image_tag }} + - name: Set image repository reference + id: image-ref + env: + IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + run: | + image="$(printf '%s' "$IMAGE" | tr '[:upper:]' '[:lower:]')" + echo "value=${image}" >> "$GITHUB_OUTPUT" + - name: Build and export to Docker uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: @@ -155,22 +168,52 @@ jobs: severity: ${{ inputs.vulnerability_severity }} hide-progress: true - - name: Push to Docker + - name: Push to Docker by digest id: push-to-docker uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: ${{ env.DOCKER_CONTEXT_PATH }} - push: true - tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ steps.image-ref.outputs.value }},push-by-digest=true,push=true build-args: | JAR_VERSION=${{ steps.update_version.outputs.new_version }} IMAGE_VERSION=${{ steps.update_version.outputs.new_version }} + BUILD_TARGET=${{ env.ENCLAVE_PROTOCOL }} + + - name: Attest image + if: ${{ !contains(steps.update_version.outputs.new_version, 'SNAPSHOT') }} + uses: IABTechLab/uid2-shared-actions/actions/attest_image@v3 + with: + subject_name: ${{ steps.image-ref.outputs.value }} + subject_digest: ${{ steps.push-to-docker.outputs.digest }} + + - name: Verify image attestation from registry + if: ${{ !contains(steps.update_version.outputs.new_version, 'SNAPSHOT') }} + uses: ./.github/actions/verify_oci_attestation + with: + subject_name: ${{ steps.image-ref.outputs.value }} + subject_digest: ${{ steps.push-to-docker.outputs.digest }} + + - name: Promote digest to Docker tags + env: + DIGEST: ${{ steps.push-to-docker.outputs.digest }} + IMAGE: ${{ steps.image-ref.outputs.value }} + TAGS: ${{ steps.meta.outputs.tags }} + run: | + set -euo pipefail + while IFS= read -r tag; do + [[ -z "$tag" ]] && continue + docker buildx imagetools create -t "$tag" "${IMAGE}@${DIGEST}" + done <<< "$TAGS" azureCc: name: Create Azure CC artifacts runs-on: ubuntu-latest - permissions: {} + permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write needs: buildImage steps: - name: Checkout @@ -192,6 +235,12 @@ jobs: run: | bash ./scripts/azure-cc/deployment/generate-deployment-artifacts.sh + - name: Attest manifest + if: ${{ !contains(needs.buildImage.outputs.jar_version, 'SNAPSHOT') }} + uses: ./.github/actions/attest_file + with: + subject_paths: ${{ env.MANIFEST_OUTPUT_DIR }}/azure-cc-operator-digest-${{ needs.buildImage.outputs.jar_version }}.txt + - name: Upload deployment artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -218,7 +267,11 @@ jobs: azureAks: name: Create Azure AKS artifacts runs-on: ubuntu-latest - permissions: {} + permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write needs: buildImage steps: - name: Checkout @@ -240,6 +293,12 @@ jobs: run: | bash ./scripts/azure-aks/deployment/generate-deployment-artifacts.sh + - name: Attest manifest + if: ${{ !contains(needs.buildImage.outputs.jar_version, 'SNAPSHOT') }} + uses: ./.github/actions/attest_file + with: + subject_paths: ${{ env.MANIFEST_OUTPUT_DIR }}/azure-aks-operator-digest-${{ needs.buildImage.outputs.jar_version }}.txt + - name: Upload deployment artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.github/workflows/publish-gcp-oidc-enclave-docker.yaml b/.github/workflows/publish-gcp-oidc-enclave-docker.yaml index 9e3f87c58..9fb87d743 100644 --- a/.github/workflows/publish-gcp-oidc-enclave-docker.yaml +++ b/.github/workflows/publish-gcp-oidc-enclave-docker.yaml @@ -67,6 +67,8 @@ jobs: security-events: write packages: write id-token: write + attestations: write + artifact-metadata: write pull-requests: write outputs: jar_version: ${{ steps.update_version.outputs.new_version }} @@ -100,28 +102,8 @@ jobs: cp -r target ${{ env.DOCKER_CONTEXT_PATH }}/ cp scripts/confidential_compute.py ${{ env.DOCKER_CONTEXT_PATH }}/ - - name: Log in to the Docker container registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Authenticate with Google Cloud - id: gcp_auth - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 - with: - token_format: access_token - workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER_ID }} - service_account: ${{ vars.GCP_PUBLISH_SERVICE_ACCOUNT }} - access_token_lifetime: 300s - - - name: Log in to the GCP Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - registry: ${{ env.GCP_REGISTRY }} - username: oauth2accesstoken - password: ${{ steps.gcp_auth.outputs.access_token }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Extract metadata (tags, labels) for Docker id: meta @@ -149,6 +131,20 @@ jobs: tags: | type=raw,value=${{ steps.update_version.outputs.new_version }}-${{ env.ENCLAVE_PROTOCOL }} + - name: Set image repository references + id: image-refs + env: + GHCR_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + GAR_IMAGE: ${{ env.GCP_REGISTRY }}/${{ env.GCP_GAR_PROJECT }}/${{ env.IMAGE_NAME }} + run: | + ghcr_image="$(printf '%s' "$GHCR_IMAGE" | tr '[:upper:]' '[:lower:]')" + gar_image="$(printf '%s' "$GAR_IMAGE" | tr '[:upper:]' '[:lower:]')" + { + echo "ghcr=${ghcr_image}" + echo "gar=${gar_image}" + echo "all=${ghcr_image},${gar_image}" + } >> "$GITHUB_OUTPUT" + - name: Build and export to Docker uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: @@ -169,17 +165,110 @@ jobs: skip_files: '/venv/lib/python3.12/site-packages/google/auth/crypt/__pycache__/_python_rsa.cpython-312.pyc' # Skip scanning this file as per UID2-4968 failure_severity: ${{ (inputs.vulnerability_severity == 'CRITICAL (DO NOT use if JIRA ticket not raised)' && 'CRITICAL') || inputs.vulnerability_severity }} - - name: Push to Docker + # Re-authenticate immediately before registry publish. Build + Trivy can exceed + # a short-lived GAR token obtained earlier in the job. + - name: Log in to the Docker container registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Authenticate with Google Cloud + id: gcp_auth + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 + with: + token_format: access_token + workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER_ID }} + service_account: ${{ vars.GCP_PUBLISH_SERVICE_ACCOUNT }} + access_token_lifetime: 3600s + + - name: Log in to the GCP Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ${{ env.GCP_REGISTRY }} + username: oauth2accesstoken + password: ${{ steps.gcp_auth.outputs.access_token }} + + - name: Push to registries by digest id: push-to-docker uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: ${{ env.DOCKER_CONTEXT_PATH }} - push: true - tags: ${{ steps.meta-all.outputs.tags }} labels: ${{ steps.meta-all.outputs.labels }} + # Quote name= so commas inside the multi-registry value are not parsed as CSV separators. + outputs: type=image,"name=${{ steps.image-refs.outputs.all }}",push-by-digest=true,push=true build-args: | JAR_VERSION=${{ steps.update_version.outputs.new_version }} IMAGE_VERSION=${{ steps.update_version.outputs.new_version }} + BUILD_TARGET=${{ env.ENCLAVE_PROTOCOL }} + + - name: Attest GHCR image + if: ${{ !contains(steps.update_version.outputs.new_version, 'SNAPSHOT') }} + uses: IABTechLab/uid2-shared-actions/actions/attest_image@v3 + with: + subject_name: ${{ steps.image-refs.outputs.ghcr }} + subject_digest: ${{ steps.push-to-docker.outputs.digest }} + + - name: Verify GHCR attestation from registry + if: ${{ !contains(steps.update_version.outputs.new_version, 'SNAPSHOT') }} + uses: ./.github/actions/verify_oci_attestation + with: + subject_name: ${{ steps.image-refs.outputs.ghcr }} + subject_digest: ${{ steps.push-to-docker.outputs.digest }} + + - name: Attest GAR image + if: ${{ !contains(steps.update_version.outputs.new_version, 'SNAPSHOT') }} + uses: IABTechLab/uid2-shared-actions/actions/attest_image@v3 + with: + subject_name: ${{ steps.image-refs.outputs.gar }} + subject_digest: ${{ steps.push-to-docker.outputs.digest }} + + - name: Verify GAR attestation from registry + if: ${{ !contains(steps.update_version.outputs.new_version, 'SNAPSHOT') }} + uses: ./.github/actions/verify_oci_attestation + with: + subject_name: ${{ steps.image-refs.outputs.gar }} + subject_digest: ${{ steps.push-to-docker.outputs.digest }} + + - name: Promote digest to registry tags + env: + DIGEST: ${{ steps.push-to-docker.outputs.digest }} + GHCR_IMAGE: ${{ steps.image-refs.outputs.ghcr }} + GHCR_TAGS: ${{ steps.meta.outputs.tags }} + GAR_IMAGE: ${{ steps.image-refs.outputs.gar }} + GAR_TAGS: ${{ steps.meta-gcp.outputs.tags }} + run: | + set -euo pipefail + + promote_tags() { + local image="$1" + local tags="$2" + while IFS= read -r tag; do + [[ -z "$tag" ]] && continue + docker buildx imagetools create -t "$tag" "${image}@${DIGEST}" + done <<< "$tags" + } + + verify_tags() { + local image="$1" + local tags="$2" + while IFS= read -r tag; do + [[ -z "$tag" ]] && continue + resolved="$(docker buildx imagetools inspect "$tag" --format '{{.Manifest.Digest}}')" + if [[ "$resolved" != "$DIGEST" ]]; then + echo "::error::Promoted tag $tag resolved to $resolved, expected $DIGEST" + exit 1 + fi + done <<< "$tags" + } + + # Promote GAR before GHCR so a mid-promotion failure is less likely to + # leave the more widely consumed GHCR version tag published alone. + promote_tags "$GAR_IMAGE" "$GAR_TAGS" + promote_tags "$GHCR_IMAGE" "$GHCR_TAGS" + verify_tags "$GAR_IMAGE" "$GAR_TAGS" + verify_tags "$GHCR_IMAGE" "$GHCR_TAGS" - name: Generate GCP deployment artifacts env: @@ -191,6 +280,14 @@ jobs: run: | bash ./scripts/gcp-oidc/generate-deployment-artifacts.sh + - name: Attest manifest artifacts + if: ${{ !contains(steps.update_version.outputs.new_version, 'SNAPSHOT') }} + uses: ./.github/actions/attest_file + with: + subject_paths: | + ${{ env.MANIFEST_OUTPUT_DIR }}/gcp-oidc-enclave-id-${{ steps.update_version.outputs.new_version }}.txt + ${{ env.MANIFEST_OUTPUT_DIR }}/gcp-oidc-enclave-id-debug-${{ steps.update_version.outputs.new_version }}.txt + - name: Upload deployment artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -210,6 +307,12 @@ jobs: run: | zip -j ${{ env.ARTIFACTS_OUTPUT_DIR }}/gcp-oidc-deployment-files-${{ steps.update_version.outputs.new_version }}.zip ${{ env.ARTIFACTS_OUTPUT_DIR }}/* + - name: Attest release archive + if: ${{ inputs.version_number_input == '' && steps.update_version.outputs.is_release == 'true' }} + uses: ./.github/actions/attest_file + with: + subject_paths: ${{ env.ARTIFACTS_OUTPUT_DIR }}/gcp-oidc-deployment-files-${{ steps.update_version.outputs.new_version }}.zip + - name: Build changelog id: github_release if: ${{ inputs.version_number_input == '' && steps.update_version.outputs.is_release == 'true' }} diff --git a/README.md b/README.md index 25bd87b3d..96d4f80c6 100644 --- a/README.md +++ b/README.md @@ -67,24 +67,104 @@ wsl trivy image ``` where `` is the built docker image you want to scan (uid2-latest in the example above). -## Verifying image provenance +## Verifying artifact provenance -Every non-snapshot image published by this repo's release workflow ships with a [SLSA v1.0](https://slsa.dev/spec/v1.0/) build-provenance attestation, signed by GitHub's [Sigstore](https://www.sigstore.dev/) instance via the OIDC identity of the [shared publish workflow](https://github.com/IABTechLab/uid2-shared-actions). The attestation cryptographically binds the image digest to the source commit, the signing workflow, and the runner that built it. +Every non-snapshot operator image and release artifact published by this repo +ships with a [SLSA v1.0](https://slsa.dev/spec/v1.0/) build-provenance +attestation, signed by GitHub's [Sigstore](https://www.sigstore.dev/) instance. +The attestation cryptographically binds the artifact digest to the source +repository, signing workflow, and GitHub-hosted runner that produced it. -To verify an image, install [`gh`](https://cli.github.com/) (≥ 2.49) and run: +Install [`gh`](https://cli.github.com/) (≥ 2.49), then use the command for the +artifact type you want to verify. Prefer `--signer-workflow` so verification +rejects attestations produced by a different workflow in the same repository. + +### Public operator image + +The public image uses the shared publish workflow: + +```bash +gh attestation verify \ + oci://ghcr.io/iabtechlab/uid2-operator: \ + --repo IABTechLab/uid2-operator \ + --signer-repo IABTechLab/uid2-shared-actions +``` + +### Private operator images + +Pin both the signing workflow and the registry-stored attestation bundle: ```bash -gh attestation verify oci://ghcr.io/iabtechlab/uid2-operator: --owner IABTechLab --signer-repo IABTechLab/uid2-shared-actions +# GCP Confidential Space image in GitHub Container Registry +gh attestation verify \ + oci://ghcr.io/iabtechlab/uid2-operator:-gcp-oidc \ + --repo IABTechLab/uid2-operator \ + --signer-workflow IABTechLab/uid2-operator/.github/workflows/publish-gcp-oidc-enclave-docker.yaml \ + --bundle-from-oci + +# The same GCP image in Google Artifact Registry +gh attestation verify \ + oci://us-docker.pkg.dev/uid2-prod-project/iabtechlab/uid2-operator:-gcp-oidc \ + --repo IABTechLab/uid2-operator \ + --signer-workflow IABTechLab/uid2-operator/.github/workflows/publish-gcp-oidc-enclave-docker.yaml \ + --bundle-from-oci + +# Azure CC/AKS image +gh attestation verify \ + oci://ghcr.io/iabtechlab/uid2-operator:-azure-cc \ + --repo IABTechLab/uid2-operator \ + --signer-workflow IABTechLab/uid2-operator/.github/workflows/publish-azure-cc-enclave-docker.yaml \ + --bundle-from-oci + +# AWS EKS Nitro images +gh attestation verify \ + oci://ghcr.io/iabtechlab/uid2-operator-eks-uid2:. \ + --repo IABTechLab/uid2-operator \ + --signer-workflow IABTechLab/uid2-operator/.github/workflows/publish-aws-eks-nitro-enclave-docker.yaml \ + --bundle-from-oci +gh attestation verify \ + oci://ghcr.io/iabtechlab/uid2-operator-eks-euid:. \ + --repo IABTechLab/uid2-operator \ + --signer-workflow IABTechLab/uid2-operator/.github/workflows/publish-aws-eks-nitro-enclave-docker.yaml \ + --bundle-from-oci ``` -`` refers to the **Docker image tag** — bare semantic version, no `v` prefix (e.g. `5.70.84`). Note that the corresponding GitHub release and git tag for the same build are named with a `v` (e.g. `v5.70.84`); the registry tag drops it by OCI convention. +When an image was published through `publish-all-operators.yaml`, use that +workflow path as `--signer-workflow` instead of the cloud-specific workflow. + +To pin an image to immutable bytes, replace its tag with the +`@sha256:` shown by the registry. + +### EIFs, measurements, and release archives -**Where to find a tag:** +Download the release assets and verify the exact file: + +```bash +gh release download v --repo IABTechLab/uid2-operator + +# Deployment or combined-manifest archive from Publish All Operators +gh attestation verify .zip \ + --repo IABTechLab/uid2-operator \ + --signer-workflow IABTechLab/uid2-operator/.github/workflows/publish-all-operators.yaml + +# An EIF or measurement file extracted from the downloaded archives +gh attestation verify /uid2operator.eif \ + --repo IABTechLab/uid2-operator \ + --signer-workflow IABTechLab/uid2-operator/.github/workflows/publish-aws-nitro-eif.yaml +gh attestation verify /.txt \ + --repo IABTechLab/uid2-operator \ + --signer-workflow IABTechLab/uid2-operator/.github/workflows/publish-all-operators.yaml +``` -- **GitHub Packages** for this repo — [`uid2-operator` package](https://github.com/IABTechLab/uid2-operator/pkgs/container/uid2-operator) lists every published image tag and its digest. -- Or take a [release](https://github.com/IABTechLab/uid2-operator/releases) name (e.g. `v5.70.84`) and drop the leading `v`. -- To pin to an exact manifest instead of a mutable tag, use the digest form: `oci://ghcr.io/iabtechlab/uid2-operator@sha256:` (visible on the Packages page, or via `gh api /orgs/IABTechLab/packages/container/uid2-operator/versions`). +AWS AMI measurement files contain AMI IDs and the EIF PCR0 used by the build. +Their attestations prove the provenance of those metadata files; they do not +represent a byte-level signature of an AWS AMI. The AMI workflow also verifies +the consumed EIF’s existing provenance before Packer runs. -A successful run prints `✓ Verification succeeded!` followed by the SLSA provenance fields — including `sourceRepositoryDigest` (the source commit), `workflow.path` (the signing workflow), and the runner identity. +A successful verification prints `✓ Verification succeeded!` and the SLSA +provenance, including `sourceRepositoryDigest`, `workflow.path`, and runner +identity. -Snapshot tags (`-SNAPSHOT` suffix) deliberately skip attestation. `gh attestation verify` returns `no attestations found` against a snapshot — that's expected. +Snapshot versions (`-SNAPSHOT`) deliberately skip attestation. +`gh attestation verify` returning `no attestations found` for a snapshot is +expected. From 5f029d0df0bc06855cbd0205d0b60b6f6e92c93b Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Mon, 17 Aug 2026 14:22:34 +1000 Subject: [PATCH 2/9] UID2-7080: Pin attestation signer workflows Use exact signer workflow policies so verification cannot accept a different workflow through permissive regular-expression matching. Co-authored-by: Cursor --- .github/actions/attest_file/action.yaml | 13 +++++-------- .github/actions/build_ami/action.yaml | 4 ++-- .github/actions/build_eks_docker_image/action.yaml | 1 + .github/actions/verify_oci_attestation/action.yaml | 12 ++++++------ .github/workflows/build-uid2-ami.yaml | 2 ++ .github/workflows/publish-all-operators.yaml | 1 + .../publish-aws-eks-nitro-enclave-docker.yaml | 1 + .github/workflows/publish-aws-nitro-eif.yaml | 4 ++++ .../workflows/publish-azure-cc-enclave-docker.yaml | 3 +++ .../workflows/publish-gcp-oidc-enclave-docker.yaml | 4 ++++ README.md | 10 ++++++---- 11 files changed, 35 insertions(+), 20 deletions(-) diff --git a/.github/actions/attest_file/action.yaml b/.github/actions/attest_file/action.yaml index f39b8c0c1..6c6b27cd8 100644 --- a/.github/actions/attest_file/action.yaml +++ b/.github/actions/attest_file/action.yaml @@ -5,6 +5,9 @@ inputs: subject_paths: description: Newline-separated paths to the files to attest. required: true + signer_workflow: + description: Exact owner/repository/path of the workflow signing the attestation. + required: true runs: using: composite @@ -19,16 +22,10 @@ runs: env: GH_TOKEN: ${{ github.token }} SUBJECT_PATHS: ${{ inputs.subject_paths }} - WORKFLOW_REF: ${{ github.workflow_ref }} + SIGNER_WORKFLOW: ${{ inputs.signer_workflow }} run: | set -euo pipefail - # Reusable workflow_call jobs are signed as the caller - # (publish-all-operators). Standalone dispatches are signed as this - # workflow file. Reject every other workflow in the repository. - signer_file="$(basename "${WORKFLOW_REF%%@*}")" - cert_identity_regex="^https://github\\.com/${GITHUB_REPOSITORY}/\\.github/workflows/(publish-all-operators\\.ya?ml|${signer_file})@" - while IFS= read -r subject_path; do [[ -z "$subject_path" ]] && continue @@ -39,5 +36,5 @@ runs: gh attestation verify "$subject_path" \ --repo "$GITHUB_REPOSITORY" \ - --cert-identity-regex "$cert_identity_regex" + --signer-workflow "$SIGNER_WORKFLOW" done <<< "$SUBJECT_PATHS" diff --git a/.github/actions/build_ami/action.yaml b/.github/actions/build_ami/action.yaml index 77d4eaee8..3b4b92d5d 100644 --- a/.github/actions/build_ami/action.yaml +++ b/.github/actions/build_ami/action.yaml @@ -135,10 +135,10 @@ runs: exit 1 fi # EIF may have been attested by the standalone Nitro workflow or by - # Publish All Operators calling that workflow via workflow_call. + # the same reusable Nitro workflow invoked by Publish All Operators. gh attestation verify "$EIF_PATH" \ --repo "$EIF_REPO" \ - --cert-identity-regex "^https://github\\.com/${EIF_REPO}/\\.github/workflows/(publish-aws-nitro-eif|publish-all-operators)\\.ya?ml@" + --signer-workflow "${EIF_REPO}/.github/workflows/publish-aws-nitro-eif.yaml" - name: Prepare EIF archive for Packer shell: bash diff --git a/.github/actions/build_eks_docker_image/action.yaml b/.github/actions/build_eks_docker_image/action.yaml index ae6143e6e..119e3386b 100644 --- a/.github/actions/build_eks_docker_image/action.yaml +++ b/.github/actions/build_eks_docker_image/action.yaml @@ -175,6 +175,7 @@ runs: with: subject_name: ${{ steps.image-ref.outputs.value }} subject_digest: ${{ steps.push-to-docker.outputs.digest }} + signer_workflow: ${{ github.repository }}/.github/workflows/publish-aws-eks-nitro-enclave-docker.yaml - name: Promote digest to Docker tags shell: bash diff --git a/.github/actions/verify_oci_attestation/action.yaml b/.github/actions/verify_oci_attestation/action.yaml index 5282efddd..8fa2592ca 100644 --- a/.github/actions/verify_oci_attestation/action.yaml +++ b/.github/actions/verify_oci_attestation/action.yaml @@ -1,8 +1,7 @@ name: Verify OCI attestation description: | Verifies that a registry-stored SLSA attestation bundle is retrievable for an - OCI image digest and was signed by this workflow or by Publish All Operators - when that workflow is the caller. + OCI image digest and was signed by the explicitly trusted workflow. inputs: subject_name: @@ -11,6 +10,9 @@ inputs: subject_digest: description: OCI manifest digest (sha256:...) emitted by docker/build-push-action. required: true + signer_workflow: + description: Exact owner/repository/path of the workflow signing the attestation. + required: true runs: using: composite @@ -21,16 +23,14 @@ runs: GH_TOKEN: ${{ github.token }} SUBJECT_NAME: ${{ inputs.subject_name }} SUBJECT_DIGEST: ${{ inputs.subject_digest }} - WORKFLOW_REF: ${{ github.workflow_ref }} + SIGNER_WORKFLOW: ${{ inputs.signer_workflow }} run: | set -euo pipefail image_ref="$(printf '%s' "$SUBJECT_NAME" | tr '[:upper:]' '[:lower:]')" - signer_file="$(basename "${WORKFLOW_REF%%@*}")" - cert_identity_regex="^https://github\\.com/${GITHUB_REPOSITORY}/\\.github/workflows/(publish-all-operators\\.ya?ml|${signer_file})@" gh attestation verify \ "oci://${image_ref}@${SUBJECT_DIGEST}" \ --bundle-from-oci \ --repo "$GITHUB_REPOSITORY" \ - --cert-identity-regex "$cert_identity_regex" + --signer-workflow "$SIGNER_WORKFLOW" diff --git a/.github/workflows/build-uid2-ami.yaml b/.github/workflows/build-uid2-ami.yaml index ee338f7c1..e66fb462b 100644 --- a/.github/workflows/build-uid2-ami.yaml +++ b/.github/workflows/build-uid2-ami.yaml @@ -63,6 +63,7 @@ jobs: uses: ./.github/actions/attest_file with: subject_paths: ${{ steps.buildAMI.outputs.measurement_path }} + signer_workflow: ${{ github.repository }}/.github/workflows/build-uid2-ami.yaml - name: Upload UID2 AMI metadata uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -129,6 +130,7 @@ jobs: uses: ./.github/actions/attest_file with: subject_paths: ${{ steps.buildAMI.outputs.measurement_path }} + signer_workflow: ${{ github.repository }}/.github/workflows/build-uid2-ami.yaml - name: Upload EUID AMI metadata uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/publish-all-operators.yaml b/.github/workflows/publish-all-operators.yaml index 0618d9f53..92ca92839 100644 --- a/.github/workflows/publish-all-operators.yaml +++ b/.github/workflows/publish-all-operators.yaml @@ -257,6 +257,7 @@ jobs: ./azure-aks-deployment-files-${{ needs.start.outputs.new_version }}.zip ./gcp-oidc-deployment-files-${{ needs.start.outputs.new_version }}.zip ./uid2-operator-release-manifests-${{ needs.start.outputs.new_version }}.zip + signer_workflow: ${{ github.repository }}/.github/workflows/publish-all-operators.yaml # Publish as a pre-release (not a draft): durable + fetchable by tag # without claiming GA. The Major-release approval gate stays the diff --git a/.github/workflows/publish-aws-eks-nitro-enclave-docker.yaml b/.github/workflows/publish-aws-eks-nitro-enclave-docker.yaml index 3bef3ab5e..eee019518 100644 --- a/.github/workflows/publish-aws-eks-nitro-enclave-docker.yaml +++ b/.github/workflows/publish-aws-eks-nitro-enclave-docker.yaml @@ -148,6 +148,7 @@ jobs: subject_paths: | ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/manifests/aws-eks-uid2-enclave-id-${{ needs.buildUID2Image.outputs.image_tag }}.txt ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/manifests/aws-eks-euid-enclave-id-${{ needs.buildEUIDImage.outputs.image_tag }}.txt + signer_workflow: ${{ github.repository }}/.github/workflows/publish-aws-eks-nitro-enclave-docker.yaml - name: Save Manifests as build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/publish-aws-nitro-eif.yaml b/.github/workflows/publish-aws-nitro-eif.yaml index 463786f3d..2dbd5a3dd 100644 --- a/.github/workflows/publish-aws-nitro-eif.yaml +++ b/.github/workflows/publish-aws-nitro-eif.yaml @@ -104,6 +104,7 @@ jobs: subject_paths: | ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/uid2/uid2operator.eif ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/uid2/pcr0.txt + signer_workflow: ${{ github.repository }}/.github/workflows/publish-aws-nitro-eif.yaml - name: Save UID2 eif artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -156,6 +157,7 @@ jobs: subject_paths: | ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/euid/uid2operator.eif ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/euid/pcr0.txt + signer_workflow: ${{ github.repository }}/.github/workflows/publish-aws-nitro-eif.yaml - name: Save EUID eif artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -215,6 +217,7 @@ jobs: subject_paths: | ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/manifests/aws-uid2-enclave-id-${{ needs.start.outputs.new_version }}.txt ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/manifests/aws-euid-enclave-id-${{ needs.start.outputs.new_version }}.txt + signer_workflow: ${{ github.repository }}/.github/workflows/publish-aws-nitro-eif.yaml - name: Save Manifests as build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -253,6 +256,7 @@ jobs: subject_paths: | ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/aws-uid2-deployment-artifacts-${{ needs.start.outputs.new_version }}.zip ${{ env.ARTIFACTS_BASE_OUTPUT_DIR }}/aws-euid-deployment-artifacts-${{ needs.start.outputs.new_version }}.zip + signer_workflow: ${{ github.repository }}/.github/workflows/publish-aws-nitro-eif.yaml - name: Create release if: ${{ inputs.version_number_input == '' && needs.start.outputs.is_release == 'true' }} diff --git a/.github/workflows/publish-azure-cc-enclave-docker.yaml b/.github/workflows/publish-azure-cc-enclave-docker.yaml index 026e871e8..281c0e8c5 100644 --- a/.github/workflows/publish-azure-cc-enclave-docker.yaml +++ b/.github/workflows/publish-azure-cc-enclave-docker.yaml @@ -193,6 +193,7 @@ jobs: with: subject_name: ${{ steps.image-ref.outputs.value }} subject_digest: ${{ steps.push-to-docker.outputs.digest }} + signer_workflow: ${{ github.repository }}/.github/workflows/publish-azure-cc-enclave-docker.yaml - name: Promote digest to Docker tags env: @@ -240,6 +241,7 @@ jobs: uses: ./.github/actions/attest_file with: subject_paths: ${{ env.MANIFEST_OUTPUT_DIR }}/azure-cc-operator-digest-${{ needs.buildImage.outputs.jar_version }}.txt + signer_workflow: ${{ github.repository }}/.github/workflows/publish-azure-cc-enclave-docker.yaml - name: Upload deployment artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -298,6 +300,7 @@ jobs: uses: ./.github/actions/attest_file with: subject_paths: ${{ env.MANIFEST_OUTPUT_DIR }}/azure-aks-operator-digest-${{ needs.buildImage.outputs.jar_version }}.txt + signer_workflow: ${{ github.repository }}/.github/workflows/publish-azure-cc-enclave-docker.yaml - name: Upload deployment artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/publish-gcp-oidc-enclave-docker.yaml b/.github/workflows/publish-gcp-oidc-enclave-docker.yaml index 9fb87d743..6127d353d 100644 --- a/.github/workflows/publish-gcp-oidc-enclave-docker.yaml +++ b/.github/workflows/publish-gcp-oidc-enclave-docker.yaml @@ -216,6 +216,7 @@ jobs: with: subject_name: ${{ steps.image-refs.outputs.ghcr }} subject_digest: ${{ steps.push-to-docker.outputs.digest }} + signer_workflow: ${{ github.repository }}/.github/workflows/publish-gcp-oidc-enclave-docker.yaml - name: Attest GAR image if: ${{ !contains(steps.update_version.outputs.new_version, 'SNAPSHOT') }} @@ -230,6 +231,7 @@ jobs: with: subject_name: ${{ steps.image-refs.outputs.gar }} subject_digest: ${{ steps.push-to-docker.outputs.digest }} + signer_workflow: ${{ github.repository }}/.github/workflows/publish-gcp-oidc-enclave-docker.yaml - name: Promote digest to registry tags env: @@ -287,6 +289,7 @@ jobs: subject_paths: | ${{ env.MANIFEST_OUTPUT_DIR }}/gcp-oidc-enclave-id-${{ steps.update_version.outputs.new_version }}.txt ${{ env.MANIFEST_OUTPUT_DIR }}/gcp-oidc-enclave-id-debug-${{ steps.update_version.outputs.new_version }}.txt + signer_workflow: ${{ github.repository }}/.github/workflows/publish-gcp-oidc-enclave-docker.yaml - name: Upload deployment artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -312,6 +315,7 @@ jobs: uses: ./.github/actions/attest_file with: subject_paths: ${{ env.ARTIFACTS_OUTPUT_DIR }}/gcp-oidc-deployment-files-${{ steps.update_version.outputs.new_version }}.zip + signer_workflow: ${{ github.repository }}/.github/workflows/publish-gcp-oidc-enclave-docker.yaml - name: Build changelog id: github_release diff --git a/README.md b/README.md index 96d4f80c6..98807cdf5 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,8 @@ gh attestation verify \ --bundle-from-oci ``` -When an image was published through `publish-all-operators.yaml`, use that -workflow path as `--signer-workflow` instead of the cloud-specific workflow. +When `publish-all-operators.yaml` invokes a cloud-specific reusable workflow, +the reusable workflow remains the signer. To pin an image to immutable bytes, replace its tag with the `@sha256:` shown by the registry. @@ -147,13 +147,15 @@ gh attestation verify .zip \ --repo IABTechLab/uid2-operator \ --signer-workflow IABTechLab/uid2-operator/.github/workflows/publish-all-operators.yaml -# An EIF or measurement file extracted from the downloaded archives +# An EIF extracted from the downloaded archives gh attestation verify /uid2operator.eif \ --repo IABTechLab/uid2-operator \ --signer-workflow IABTechLab/uid2-operator/.github/workflows/publish-aws-nitro-eif.yaml + +# A measurement file (use the workflow that generated that measurement) gh attestation verify /.txt \ --repo IABTechLab/uid2-operator \ - --signer-workflow IABTechLab/uid2-operator/.github/workflows/publish-all-operators.yaml + --signer-workflow IABTechLab/uid2-operator/.github/workflows/.yaml ``` AWS AMI measurement files contain AMI IDs and the EIF PCR0 used by the build. From 86996d52c16416755471c99d97b214b51676ca1a Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Mon, 17 Aug 2026 14:40:51 +1000 Subject: [PATCH 3/9] UID2-7080: Verify promoted private image tags Confirm Azure and EKS version tags resolve to the attested digest before their publish workflows continue. Co-authored-by: Cursor --- .github/actions/build_eks_docker_image/action.yaml | 9 +++++++++ .github/workflows/publish-azure-cc-enclave-docker.yaml | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/.github/actions/build_eks_docker_image/action.yaml b/.github/actions/build_eks_docker_image/action.yaml index 119e3386b..4740e4b04 100644 --- a/.github/actions/build_eks_docker_image/action.yaml +++ b/.github/actions/build_eks_docker_image/action.yaml @@ -190,6 +190,15 @@ runs: docker buildx imagetools create -t "$tag" "${IMAGE}@${DIGEST}" done <<< "$TAGS" + while IFS= read -r tag; do + [[ -z "$tag" ]] && continue + resolved="$(docker buildx imagetools inspect "$tag" --format '{{.Manifest.Digest}}')" + if [[ "$resolved" != "$DIGEST" ]]; then + echo "::error::Promoted tag $tag resolved to $resolved, expected $DIGEST" + exit 1 + fi + done <<< "$TAGS" + - name: Check disk usage shell: bash run: | diff --git a/.github/workflows/publish-azure-cc-enclave-docker.yaml b/.github/workflows/publish-azure-cc-enclave-docker.yaml index 281c0e8c5..fcc12c954 100644 --- a/.github/workflows/publish-azure-cc-enclave-docker.yaml +++ b/.github/workflows/publish-azure-cc-enclave-docker.yaml @@ -207,6 +207,15 @@ jobs: docker buildx imagetools create -t "$tag" "${IMAGE}@${DIGEST}" done <<< "$TAGS" + while IFS= read -r tag; do + [[ -z "$tag" ]] && continue + resolved="$(docker buildx imagetools inspect "$tag" --format '{{.Manifest.Digest}}')" + if [[ "$resolved" != "$DIGEST" ]]; then + echo "::error::Promoted tag $tag resolved to $resolved, expected $DIGEST" + exit 1 + fi + done <<< "$TAGS" + azureCc: name: Create Azure CC artifacts runs-on: ubuntu-latest From f43ec6a2b69931487ea32a3a6b366b49db8698e4 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Tue, 18 Aug 2026 08:46:10 +1000 Subject: [PATCH 4/9] UID2-7080: Preserve standalone EIF release permission Grant the cleanup job contents write access so its standalone release path can create the draft GitHub Release after provenance checks pass. Co-authored-by: Cursor --- .github/workflows/publish-aws-nitro-eif.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-aws-nitro-eif.yaml b/.github/workflows/publish-aws-nitro-eif.yaml index 2dbd5a3dd..3a7cda119 100644 --- a/.github/workflows/publish-aws-nitro-eif.yaml +++ b/.github/workflows/publish-aws-nitro-eif.yaml @@ -179,7 +179,7 @@ jobs: runs-on: ubuntu-latest needs: [start, buildUID2EIF, buildEUIDEIF] permissions: - contents: read + contents: write id-token: write attestations: write artifact-metadata: write From 143b57cf58eeefacd16340cbb143986b50328905 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Tue, 18 Aug 2026 08:50:55 +1000 Subject: [PATCH 5/9] UID2-7080: Preserve changelog pull request access Grant release jobs read access to pull requests so changelog generation can enumerate merged PR metadata under explicit token permissions. Co-authored-by: Cursor --- .github/workflows/publish-all-operators.yaml | 1 + .github/workflows/publish-aws-nitro-eif.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/publish-all-operators.yaml b/.github/workflows/publish-all-operators.yaml index 92ca92839..6c94443da 100644 --- a/.github/workflows/publish-all-operators.yaml +++ b/.github/workflows/publish-all-operators.yaml @@ -161,6 +161,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + pull-requests: read id-token: write attestations: write artifact-metadata: write diff --git a/.github/workflows/publish-aws-nitro-eif.yaml b/.github/workflows/publish-aws-nitro-eif.yaml index 3a7cda119..682f90428 100644 --- a/.github/workflows/publish-aws-nitro-eif.yaml +++ b/.github/workflows/publish-aws-nitro-eif.yaml @@ -180,6 +180,7 @@ jobs: needs: [start, buildUID2EIF, buildEUIDEIF] permissions: contents: write + pull-requests: read id-token: write attestations: write artifact-metadata: write From 5004ac329512385cd34b53df6104468b049f1a2d Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Tue, 18 Aug 2026 08:56:06 +1000 Subject: [PATCH 6/9] UID2-7080: Document legacy EIF rebuild limitation Make pre-provenance AMI rebuild failures recognisable and record that the workflow currently has no compatibility bypass. Co-authored-by: Cursor --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 98807cdf5..8af2fda45 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,19 @@ Their attestations prove the provenance of those metadata files; they do not represent a byte-level signature of an AWS AMI. The AMI workflow also verifies the consumed EIF’s existing provenance before Packer runs. +### Rebuilding AMIs from legacy EIFs + +The AMI workflow fails closed when a non-snapshot EIF has no provenance +attestation. EIFs produced before provenance enforcement was introduced cannot +therefore be used to rebuild or repair an AMI with the current workflow. The +cutover is the first non-snapshot operator release published after this +workflow change. + +For a legacy EIF, `gh attestation verify` reports `no attestations found` +before Packer starts. This indicates a pre-provenance artifact, not necessarily +artifact corruption. There is currently no bypass; supporting a legacy AMI +rebuild requires a reviewed workflow change. + A successful verification prints `✓ Verification succeeded!` and the SLSA provenance, including `sourceRepositoryDigest`, `workflow.path`, and runner identity. From a25bca52b8db8828ff195f5b32ba99a5e5cd6ee8 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Tue, 18 Aug 2026 09:07:54 +1000 Subject: [PATCH 7/9] UID2-7080: Preserve private image manifest format Disable BuildKit's redundant implicit provenance so private images remain plain single-platform manifests while signed SLSA attestations are published separately. Co-authored-by: Cursor --- .github/actions/build_eks_docker_image/action.yaml | 3 +++ .github/workflows/publish-azure-cc-enclave-docker.yaml | 3 +++ .github/workflows/publish-gcp-oidc-enclave-docker.yaml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/.github/actions/build_eks_docker_image/action.yaml b/.github/actions/build_eks_docker_image/action.yaml index 4740e4b04..1e530b6d7 100644 --- a/.github/actions/build_eks_docker_image/action.yaml +++ b/.github/actions/build_eks_docker_image/action.yaml @@ -157,6 +157,9 @@ runs: context: ${{ inputs.artifacts_output_dir }} labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ steps.image-ref.outputs.value }},push-by-digest=true,push=true + # Keep the private image as a plain single-platform manifest. Signed + # SLSA provenance is added separately by attest_image below. + provenance: false build-args: | JAR_VERSION=${{ steps.versionNumber.outputs.VERSION_NUMBER }} IMAGE_VERSION=${{ steps.versionNumber.outputs.VERSION_NUMBER }} diff --git a/.github/workflows/publish-azure-cc-enclave-docker.yaml b/.github/workflows/publish-azure-cc-enclave-docker.yaml index fcc12c954..c26623aa7 100644 --- a/.github/workflows/publish-azure-cc-enclave-docker.yaml +++ b/.github/workflows/publish-azure-cc-enclave-docker.yaml @@ -175,6 +175,9 @@ jobs: context: ${{ env.DOCKER_CONTEXT_PATH }} labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ steps.image-ref.outputs.value }},push-by-digest=true,push=true + # Keep the private image as a plain single-platform manifest. Signed + # SLSA provenance is added separately by attest_image below. + provenance: false build-args: | JAR_VERSION=${{ steps.update_version.outputs.new_version }} IMAGE_VERSION=${{ steps.update_version.outputs.new_version }} diff --git a/.github/workflows/publish-gcp-oidc-enclave-docker.yaml b/.github/workflows/publish-gcp-oidc-enclave-docker.yaml index 6127d353d..c87275df9 100644 --- a/.github/workflows/publish-gcp-oidc-enclave-docker.yaml +++ b/.github/workflows/publish-gcp-oidc-enclave-docker.yaml @@ -198,6 +198,9 @@ jobs: labels: ${{ steps.meta-all.outputs.labels }} # Quote name= so commas inside the multi-registry value are not parsed as CSV separators. outputs: type=image,"name=${{ steps.image-refs.outputs.all }}",push-by-digest=true,push=true + # Keep the private image as a plain single-platform manifest. Signed + # SLSA provenance is added separately by attest_image below. + provenance: false build-args: | JAR_VERSION=${{ steps.update_version.outputs.new_version }} IMAGE_VERSION=${{ steps.update_version.outputs.new_version }} From 77a5e4835212a89b2428a17edf5b888fc1927636 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Tue, 18 Aug 2026 09:34:59 +1000 Subject: [PATCH 8/9] UID2-7080: Preserve digest during tag promotion Disable imagetools index preference so private image tags resolve directly to the plain manifest that was attested and verified. Co-authored-by: Cursor --- .github/actions/build_eks_docker_image/action.yaml | 2 +- .github/workflows/publish-azure-cc-enclave-docker.yaml | 2 +- .github/workflows/publish-gcp-oidc-enclave-docker.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/build_eks_docker_image/action.yaml b/.github/actions/build_eks_docker_image/action.yaml index 1e530b6d7..9260adcaa 100644 --- a/.github/actions/build_eks_docker_image/action.yaml +++ b/.github/actions/build_eks_docker_image/action.yaml @@ -190,7 +190,7 @@ runs: set -euo pipefail while IFS= read -r tag; do [[ -z "$tag" ]] && continue - docker buildx imagetools create -t "$tag" "${IMAGE}@${DIGEST}" + docker buildx imagetools create --prefer-index=false -t "$tag" "${IMAGE}@${DIGEST}" done <<< "$TAGS" while IFS= read -r tag; do diff --git a/.github/workflows/publish-azure-cc-enclave-docker.yaml b/.github/workflows/publish-azure-cc-enclave-docker.yaml index c26623aa7..33034fcb3 100644 --- a/.github/workflows/publish-azure-cc-enclave-docker.yaml +++ b/.github/workflows/publish-azure-cc-enclave-docker.yaml @@ -207,7 +207,7 @@ jobs: set -euo pipefail while IFS= read -r tag; do [[ -z "$tag" ]] && continue - docker buildx imagetools create -t "$tag" "${IMAGE}@${DIGEST}" + docker buildx imagetools create --prefer-index=false -t "$tag" "${IMAGE}@${DIGEST}" done <<< "$TAGS" while IFS= read -r tag; do diff --git a/.github/workflows/publish-gcp-oidc-enclave-docker.yaml b/.github/workflows/publish-gcp-oidc-enclave-docker.yaml index c87275df9..d330c38b9 100644 --- a/.github/workflows/publish-gcp-oidc-enclave-docker.yaml +++ b/.github/workflows/publish-gcp-oidc-enclave-docker.yaml @@ -251,7 +251,7 @@ jobs: local tags="$2" while IFS= read -r tag; do [[ -z "$tag" ]] && continue - docker buildx imagetools create -t "$tag" "${image}@${DIGEST}" + docker buildx imagetools create --prefer-index=false -t "$tag" "${image}@${DIGEST}" done <<< "$tags" } From 03f2f0017e8f069a357a612ec3cc47d9efc102e3 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Tue, 18 Aug 2026 10:50:31 +1000 Subject: [PATCH 9/9] UID2-7080: Pin private images to Docker media types Preserve the historical Docker v2 manifest format despite BuildKit's newer OCI media type default. Co-authored-by: Cursor --- .github/actions/build_eks_docker_image/action.yaml | 2 +- .github/workflows/publish-azure-cc-enclave-docker.yaml | 2 +- .github/workflows/publish-gcp-oidc-enclave-docker.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/build_eks_docker_image/action.yaml b/.github/actions/build_eks_docker_image/action.yaml index 9260adcaa..410c10a55 100644 --- a/.github/actions/build_eks_docker_image/action.yaml +++ b/.github/actions/build_eks_docker_image/action.yaml @@ -156,7 +156,7 @@ runs: with: context: ${{ inputs.artifacts_output_dir }} labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ steps.image-ref.outputs.value }},push-by-digest=true,push=true + outputs: type=image,name=${{ steps.image-ref.outputs.value }},oci-mediatypes=false,push-by-digest=true,push=true # Keep the private image as a plain single-platform manifest. Signed # SLSA provenance is added separately by attest_image below. provenance: false diff --git a/.github/workflows/publish-azure-cc-enclave-docker.yaml b/.github/workflows/publish-azure-cc-enclave-docker.yaml index 33034fcb3..3b21aee5d 100644 --- a/.github/workflows/publish-azure-cc-enclave-docker.yaml +++ b/.github/workflows/publish-azure-cc-enclave-docker.yaml @@ -174,7 +174,7 @@ jobs: with: context: ${{ env.DOCKER_CONTEXT_PATH }} labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ steps.image-ref.outputs.value }},push-by-digest=true,push=true + outputs: type=image,name=${{ steps.image-ref.outputs.value }},oci-mediatypes=false,push-by-digest=true,push=true # Keep the private image as a plain single-platform manifest. Signed # SLSA provenance is added separately by attest_image below. provenance: false diff --git a/.github/workflows/publish-gcp-oidc-enclave-docker.yaml b/.github/workflows/publish-gcp-oidc-enclave-docker.yaml index d330c38b9..ec62f5603 100644 --- a/.github/workflows/publish-gcp-oidc-enclave-docker.yaml +++ b/.github/workflows/publish-gcp-oidc-enclave-docker.yaml @@ -197,7 +197,7 @@ jobs: context: ${{ env.DOCKER_CONTEXT_PATH }} labels: ${{ steps.meta-all.outputs.labels }} # Quote name= so commas inside the multi-registry value are not parsed as CSV separators. - outputs: type=image,"name=${{ steps.image-refs.outputs.all }}",push-by-digest=true,push=true + outputs: type=image,"name=${{ steps.image-refs.outputs.all }}",oci-mediatypes=false,push-by-digest=true,push=true # Keep the private image as a plain single-platform manifest. Signed # SLSA provenance is added separately by attest_image below. provenance: false