@@ -122,7 +122,11 @@ jobs:
122122# Necessary to push docker images to ghcr.io.
123123packages :write
124124# Necessary for GCP authentication (https://github.com/google-github-actions/setup-gcloud#usage)
125+ # Also necessary for keyless cosign (https://docs.sigstore.dev/cosign/signing/overview/)
126+ # And for GitHub Actions attestation
125127id-token :write
128+ # Required for GitHub Actions attestation
129+ attestations :write
126130env :
127131# Necessary for Docker manifest
128132DOCKER_CLI_EXPERIMENTAL :" enabled"
@@ -246,6 +250,16 @@ jobs:
246250 apple-codesign-0.22.0-x86_64-unknown-linux-musl/rcodesign
247251 rm /tmp/rcodesign.tar.gz
248252
253+ -name :Install cosign
254+ uses :sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
255+ with :
256+ cosign-release :" v2.4.3"
257+
258+ -name :Install syft
259+ uses :anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0
260+ with :
261+ syft-version :" v1.20.0"
262+
249263 -name :Setup Apple Developer certificate and API key
250264run :|
251265 set -euo pipefail
@@ -361,6 +375,7 @@ jobs:
361375file :scripts/Dockerfile.base
362376platforms :linux/amd64,linux/arm64,linux/arm/v7
363377provenance :true
378+ sbom :true
364379pull :true
365380no-cache :true
366381push :true
@@ -397,7 +412,52 @@ jobs:
397412 echo "$manifests" | grep -q linux/arm64
398413 echo "$manifests" | grep -q linux/arm/v7
399414
415+ # GitHub attestation provides SLSA provenance for Docker images, establishing a verifiable
416+ # record that these images were built in GitHub Actions with specific inputs and environment.
417+ # This complements our existing cosign attestations (which focus on SBOMs) by adding
418+ # GitHub-specific build provenance to enhance our supply chain security.
419+ #
420+ # TODO: Consider refactoring these attestation steps to use a matrix strategy or composite action
421+ # to reduce duplication while maintaining the required functionality for each distinct image tag.
422+ -name :GitHub Attestation for Base Docker image
423+ id :attest_base
424+ if :${{ !inputs.dry_run && steps.image-base-tag.outputs.tag != '' }}
425+ continue-on-error :true
426+ uses :actions/attest@a63cfcc7d1aab266ee064c58250cfc2c7d07bc31 # v2.2.1
427+ with :
428+ subject-name :${{ steps.image-base-tag.outputs.tag }}
429+ predicate-type :" https://slsa.dev/provenance/v1"
430+ predicate :|
431+ {
432+ "buildType": "https://github.com/actions/runner-images/",
433+ "builder": {
434+ "id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
435+ },
436+ "invocation": {
437+ "configSource": {
438+ "uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
439+ "digest": {
440+ "sha1": "${{ github.sha }}"
441+ },
442+ "entryPoint": ".github/workflows/release.yaml"
443+ },
444+ "environment": {
445+ "github_workflow": "${{ github.workflow }}",
446+ "github_run_id": "${{ github.run_id }}"
447+ }
448+ },
449+ "metadata": {
450+ "buildInvocationID": "${{ github.run_id }}",
451+ "completeness": {
452+ "environment": true,
453+ "materials":true
454+ }
455+ }
456+ }
457+ push-to-registry :true
458+
400459 -name :Build Linux Docker images
460+ id :build_docker
401461run :|
402462 set -euxo pipefail
403463
@@ -416,18 +476,125 @@ jobs:
416476 # being pushed so will automatically push them.
417477 make push/build/coder_"$version"_linux.tag
418478
479+ # Save multiarch image tag for attestation
480+ multiarch_image="$(./scripts/image_tag.sh)"
481+ echo "multiarch_image=${multiarch_image}" >> $GITHUB_OUTPUT
482+
483+ # For debugging, print all docker image tags
484+ docker images
485+
419486 # if the current version is equal to the highest (according to semver)
420487 # version in the repo, also create a multi-arch image as ":latest" and
421488 # push it
489+ created_latest_tag=false
422490 if [[ "$(git tag | grep '^v' | grep -vE '(rc|dev|-|\+|\/)' | sort -r --version-sort | head -n1)" == "v$(./scripts/version.sh)" ]]; then
423491 ./scripts/build_docker_multiarch.sh \
424492 --push \
425493 --target "$(./scripts/image_tag.sh --version latest)" \
426494 $(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag)
495+ created_latest_tag=true
496+ echo "created_latest_tag=true" >> $GITHUB_OUTPUT
497+ else
498+ echo "created_latest_tag=false" >> $GITHUB_OUTPUT
427499 fi
428500env :
429501CODER_BASE_IMAGE_TAG :${{ steps.image-base-tag.outputs.tag }}
430502
503+ -name :GitHub Attestation for Docker image
504+ id :attest_main
505+ if :${{ !inputs.dry_run }}
506+ continue-on-error :true
507+ uses :actions/attest@a63cfcc7d1aab266ee064c58250cfc2c7d07bc31 # v2.2.1
508+ with :
509+ subject-name :${{ steps.build_docker.outputs.multiarch_image }}
510+ predicate-type :" https://slsa.dev/provenance/v1"
511+ predicate :|
512+ {
513+ "buildType": "https://github.com/actions/runner-images/",
514+ "builder": {
515+ "id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
516+ },
517+ "invocation": {
518+ "configSource": {
519+ "uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
520+ "digest": {
521+ "sha1": "${{ github.sha }}"
522+ },
523+ "entryPoint": ".github/workflows/release.yaml"
524+ },
525+ "environment": {
526+ "github_workflow": "${{ github.workflow }}",
527+ "github_run_id": "${{ github.run_id }}"
528+ }
529+ },
530+ "metadata": {
531+ "buildInvocationID": "${{ github.run_id }}",
532+ "completeness": {
533+ "environment": true,
534+ "materials":true
535+ }
536+ }
537+ }
538+ push-to-registry :true
539+
540+ # Get the latest tag name for attestation
541+ -name :Get latest tag name
542+ id :latest_tag
543+ if :${{ !inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true' }}
544+ run :echo "tag=$(./scripts/image_tag.sh --version latest)" >> $GITHUB_OUTPUT
545+
546+ # If this is the highest version according to semver, also attest the "latest" tag
547+ -name :GitHub Attestation for "latest" Docker image
548+ id :attest_latest
549+ if :${{ !inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true' }}
550+ continue-on-error :true
551+ uses :actions/attest@a63cfcc7d1aab266ee064c58250cfc2c7d07bc31 # v2.2.1
552+ with :
553+ subject-name :${{ steps.latest_tag.outputs.tag }}
554+ predicate-type :" https://slsa.dev/provenance/v1"
555+ predicate :|
556+ {
557+ "buildType": "https://github.com/actions/runner-images/",
558+ "builder": {
559+ "id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
560+ },
561+ "invocation": {
562+ "configSource": {
563+ "uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
564+ "digest": {
565+ "sha1": "${{ github.sha }}"
566+ },
567+ "entryPoint": ".github/workflows/release.yaml"
568+ },
569+ "environment": {
570+ "github_workflow": "${{ github.workflow }}",
571+ "github_run_id": "${{ github.run_id }}"
572+ }
573+ },
574+ "metadata": {
575+ "buildInvocationID": "${{ github.run_id }}",
576+ "completeness": {
577+ "environment": true,
578+ "materials":true
579+ }
580+ }
581+ }
582+ push-to-registry :true
583+
584+ # Report attestation failures but don't fail the workflow
585+ -name :Check attestation status
586+ if :${{ !inputs.dry_run }}
587+ run :|
588+ if [[ "${{ steps.attest_base.outcome }}" == "failure" && "${{ steps.attest_base.conclusion }}" != "skipped" ]]; then
589+ echo "::warning::GitHub attestation for base image failed"
590+ fi
591+ if [[ "${{ steps.attest_main.outcome }}" == "failure" ]]; then
592+ echo "::warning::GitHub attestation for main image failed"
593+ fi
594+ if [[ "${{ steps.attest_latest.outcome }}" == "failure" && "${{ steps.attest_latest.conclusion }}" != "skipped" ]]; then
595+ echo "::warning::GitHub attestation for latest image failed"
596+ fi
597+
431598 -name :Generate offline docs
432599run :|
433600 version="$(./scripts/version.sh)"