chore: enable SBOM attestations for docker images (#16894)

- Enable SBOM and provenance attestations in Docker builds
- Installs `cosign` and `syft` in dogfood image
- Adds [github
attestations](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds)

Signed-off-by: Thomas Kosiewski <tk@coder.com>

---------

Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Thomas Kosiewski <tk@coder.com>
This commit is contained in:
M Atif Ali
2025-03-13 21:45:11 +05:00
committed by GitHub
parent 30179aeaac
commit 4987de654e
5 changed files with 339 additions and 3 deletions

View File

@ -1024,7 +1024,11 @@ jobs:
# Necessary to push docker images to ghcr.io.
packages: write
# Necessary for GCP authentication (https://github.com/google-github-actions/setup-gcloud#usage)
# Also necessary for keyless cosign (https://docs.sigstore.dev/cosign/signing/overview/)
# And for GitHub Actions attestation
id-token: write
# Required for GitHub Actions attestation
attestations: write
env:
DOCKER_CLI_EXPERIMENTAL: "enabled"
outputs:
@ -1069,6 +1073,16 @@ jobs:
- name: Install zstd
run: sudo apt-get install -y zstd
- name: Install cosign
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
with:
cosign-release: "v2.4.3"
- name: Install syft
uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0
with:
syft-version: "v1.20.0"
- name: Setup Windows EV Signing Certificate
run: |
set -euo pipefail
@ -1170,6 +1184,138 @@ jobs:
done
fi
# GitHub attestation provides SLSA provenance for the Docker images, establishing a verifiable
# record that these images were built in GitHub Actions with specific inputs and environment.
# This complements our existing cosign attestations which focus on SBOMs.
#
# We attest each tag separately to ensure all tags have proper provenance records.
# TODO: Consider refactoring these steps to use a matrix strategy or composite action to reduce duplication
# while maintaining the required functionality for each tag.
- name: GitHub Attestation for Docker image
id: attest_main
if: github.ref == 'refs/heads/main'
continue-on-error: true
uses: actions/attest@a63cfcc7d1aab266ee064c58250cfc2c7d07bc31 # v2.2.1
with:
subject-name: "ghcr.io/coder/coder-preview:main"
predicate-type: "https://slsa.dev/provenance/v1"
predicate: |
{
"buildType": "https://github.com/actions/runner-images/",
"builder": {
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
},
"invocation": {
"configSource": {
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
"digest": {
"sha1": "${{ github.sha }}"
},
"entryPoint": ".github/workflows/ci.yaml"
},
"environment": {
"github_workflow": "${{ github.workflow }}",
"github_run_id": "${{ github.run_id }}"
}
},
"metadata": {
"buildInvocationID": "${{ github.run_id }}",
"completeness": {
"environment": true,
"materials": true
}
}
}
push-to-registry: true
- name: GitHub Attestation for Docker image (latest tag)
id: attest_latest
if: github.ref == 'refs/heads/main'
continue-on-error: true
uses: actions/attest@a63cfcc7d1aab266ee064c58250cfc2c7d07bc31 # v2.2.1
with:
subject-name: "ghcr.io/coder/coder-preview:latest"
predicate-type: "https://slsa.dev/provenance/v1"
predicate: |
{
"buildType": "https://github.com/actions/runner-images/",
"builder": {
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
},
"invocation": {
"configSource": {
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
"digest": {
"sha1": "${{ github.sha }}"
},
"entryPoint": ".github/workflows/ci.yaml"
},
"environment": {
"github_workflow": "${{ github.workflow }}",
"github_run_id": "${{ github.run_id }}"
}
},
"metadata": {
"buildInvocationID": "${{ github.run_id }}",
"completeness": {
"environment": true,
"materials": true
}
}
}
push-to-registry: true
- name: GitHub Attestation for version-specific Docker image
id: attest_version
if: github.ref == 'refs/heads/main'
continue-on-error: true
uses: actions/attest@a63cfcc7d1aab266ee064c58250cfc2c7d07bc31 # v2.2.1
with:
subject-name: "ghcr.io/coder/coder-preview:${{ steps.build-docker.outputs.tag }}"
predicate-type: "https://slsa.dev/provenance/v1"
predicate: |
{
"buildType": "https://github.com/actions/runner-images/",
"builder": {
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
},
"invocation": {
"configSource": {
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
"digest": {
"sha1": "${{ github.sha }}"
},
"entryPoint": ".github/workflows/ci.yaml"
},
"environment": {
"github_workflow": "${{ github.workflow }}",
"github_run_id": "${{ github.run_id }}"
}
},
"metadata": {
"buildInvocationID": "${{ github.run_id }}",
"completeness": {
"environment": true,
"materials": true
}
}
}
push-to-registry: true
# Report attestation failures but don't fail the workflow
- name: Check attestation status
if: github.ref == 'refs/heads/main'
run: |
if [[ "${{ steps.attest_main.outcome }}" == "failure" ]]; then
echo "::warning::GitHub attestation for main tag failed"
fi
if [[ "${{ steps.attest_latest.outcome }}" == "failure" ]]; then
echo "::warning::GitHub attestation for latest tag failed"
fi
if [[ "${{ steps.attest_version.outcome }}" == "failure" ]]; then
echo "::warning::GitHub attestation for version-specific tag failed"
fi
- name: Prune old images
if: github.ref == 'refs/heads/main'
uses: vlaurin/action-ghcr-prune@0cf7d39f88546edd31965acba78cdcb0be14d641 # v0.6.0

View File

@ -122,7 +122,11 @@ jobs:
# Necessary to push docker images to ghcr.io.
packages: write
# Necessary for GCP authentication (https://github.com/google-github-actions/setup-gcloud#usage)
# Also necessary for keyless cosign (https://docs.sigstore.dev/cosign/signing/overview/)
# And for GitHub Actions attestation
id-token: write
# Required for GitHub Actions attestation
attestations: write
env:
# Necessary for Docker manifest
DOCKER_CLI_EXPERIMENTAL: "enabled"
@ -246,6 +250,16 @@ jobs:
apple-codesign-0.22.0-x86_64-unknown-linux-musl/rcodesign
rm /tmp/rcodesign.tar.gz
- name: Install cosign
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
with:
cosign-release: "v2.4.3"
- name: Install syft
uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0
with:
syft-version: "v1.20.0"
- name: Setup Apple Developer certificate and API key
run: |
set -euo pipefail
@ -361,6 +375,7 @@ jobs:
file: scripts/Dockerfile.base
platforms: linux/amd64,linux/arm64,linux/arm/v7
provenance: true
sbom: true
pull: true
no-cache: true
push: true
@ -397,7 +412,52 @@ jobs:
echo "$manifests" | grep -q linux/arm64
echo "$manifests" | grep -q linux/arm/v7
# GitHub attestation provides SLSA provenance for Docker images, establishing a verifiable
# record that these images were built in GitHub Actions with specific inputs and environment.
# This complements our existing cosign attestations (which focus on SBOMs) by adding
# GitHub-specific build provenance to enhance our supply chain security.
#
# TODO: Consider refactoring these attestation steps to use a matrix strategy or composite action
# to reduce duplication while maintaining the required functionality for each distinct image tag.
- name: GitHub Attestation for Base Docker image
id: attest_base
if: ${{ !inputs.dry_run && steps.image-base-tag.outputs.tag != '' }}
continue-on-error: true
uses: actions/attest@a63cfcc7d1aab266ee064c58250cfc2c7d07bc31 # v2.2.1
with:
subject-name: ${{ steps.image-base-tag.outputs.tag }}
predicate-type: "https://slsa.dev/provenance/v1"
predicate: |
{
"buildType": "https://github.com/actions/runner-images/",
"builder": {
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
},
"invocation": {
"configSource": {
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
"digest": {
"sha1": "${{ github.sha }}"
},
"entryPoint": ".github/workflows/release.yaml"
},
"environment": {
"github_workflow": "${{ github.workflow }}",
"github_run_id": "${{ github.run_id }}"
}
},
"metadata": {
"buildInvocationID": "${{ github.run_id }}",
"completeness": {
"environment": true,
"materials": true
}
}
}
push-to-registry: true
- name: Build Linux Docker images
id: build_docker
run: |
set -euxo pipefail
@ -416,18 +476,125 @@ jobs:
# being pushed so will automatically push them.
make push/build/coder_"$version"_linux.tag
# Save multiarch image tag for attestation
multiarch_image="$(./scripts/image_tag.sh)"
echo "multiarch_image=${multiarch_image}" >> $GITHUB_OUTPUT
# For debugging, print all docker image tags
docker images
# if the current version is equal to the highest (according to semver)
# version in the repo, also create a multi-arch image as ":latest" and
# push it
created_latest_tag=false
if [[ "$(git tag | grep '^v' | grep -vE '(rc|dev|-|\+|\/)' | sort -r --version-sort | head -n1)" == "v$(./scripts/version.sh)" ]]; then
./scripts/build_docker_multiarch.sh \
--push \
--target "$(./scripts/image_tag.sh --version latest)" \
$(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag)
created_latest_tag=true
echo "created_latest_tag=true" >> $GITHUB_OUTPUT
else
echo "created_latest_tag=false" >> $GITHUB_OUTPUT
fi
env:
CODER_BASE_IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }}
- name: GitHub Attestation for Docker image
id: attest_main
if: ${{ !inputs.dry_run }}
continue-on-error: true
uses: actions/attest@a63cfcc7d1aab266ee064c58250cfc2c7d07bc31 # v2.2.1
with:
subject-name: ${{ steps.build_docker.outputs.multiarch_image }}
predicate-type: "https://slsa.dev/provenance/v1"
predicate: |
{
"buildType": "https://github.com/actions/runner-images/",
"builder": {
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
},
"invocation": {
"configSource": {
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
"digest": {
"sha1": "${{ github.sha }}"
},
"entryPoint": ".github/workflows/release.yaml"
},
"environment": {
"github_workflow": "${{ github.workflow }}",
"github_run_id": "${{ github.run_id }}"
}
},
"metadata": {
"buildInvocationID": "${{ github.run_id }}",
"completeness": {
"environment": true,
"materials": true
}
}
}
push-to-registry: true
# Get the latest tag name for attestation
- name: Get latest tag name
id: latest_tag
if: ${{ !inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true' }}
run: echo "tag=$(./scripts/image_tag.sh --version latest)" >> $GITHUB_OUTPUT
# If this is the highest version according to semver, also attest the "latest" tag
- name: GitHub Attestation for "latest" Docker image
id: attest_latest
if: ${{ !inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true' }}
continue-on-error: true
uses: actions/attest@a63cfcc7d1aab266ee064c58250cfc2c7d07bc31 # v2.2.1
with:
subject-name: ${{ steps.latest_tag.outputs.tag }}
predicate-type: "https://slsa.dev/provenance/v1"
predicate: |
{
"buildType": "https://github.com/actions/runner-images/",
"builder": {
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
},
"invocation": {
"configSource": {
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
"digest": {
"sha1": "${{ github.sha }}"
},
"entryPoint": ".github/workflows/release.yaml"
},
"environment": {
"github_workflow": "${{ github.workflow }}",
"github_run_id": "${{ github.run_id }}"
}
},
"metadata": {
"buildInvocationID": "${{ github.run_id }}",
"completeness": {
"environment": true,
"materials": true
}
}
}
push-to-registry: true
# Report attestation failures but don't fail the workflow
- name: Check attestation status
if: ${{ !inputs.dry_run }}
run: |
if [[ "${{ steps.attest_base.outcome }}" == "failure" && "${{ steps.attest_base.conclusion }}" != "skipped" ]]; then
echo "::warning::GitHub attestation for base image failed"
fi
if [[ "${{ steps.attest_main.outcome }}" == "failure" ]]; then
echo "::warning::GitHub attestation for main image failed"
fi
if [[ "${{ steps.attest_latest.outcome }}" == "failure" && "${{ steps.attest_latest.conclusion }}" != "skipped" ]]; then
echo "::warning::GitHub attestation for latest image failed"
fi
- name: Generate offline docs
run: |
version="$(./scripts/version.sh)"

View File

@ -9,7 +9,7 @@ RUN cargo install exa bat ripgrep typos-cli watchexec-cli && \
FROM ubuntu:jammy@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 AS go
# Install Go manually, so that we can control the version
ARG GO_VERSION=1.22.8
ARG GO_VERSION=1.24.1
# Boring Go is needed to build FIPS-compliant binaries.
RUN apt-get update && \
@ -278,7 +278,9 @@ ARG CLOUD_SQL_PROXY_VERSION=2.2.0 \
KUBECTX_VERSION=0.9.4 \
STRIPE_VERSION=1.14.5 \
TERRAGRUNT_VERSION=0.45.11 \
TRIVY_VERSION=0.41.0
TRIVY_VERSION=0.41.0 \
SYFT_VERSION=1.20.0 \
COSIGN_VERSION=2.4.3
# cloud_sql_proxy, for connecting to cloudsql instances
# the upstream go.mod prevents this from being installed with go install
@ -316,7 +318,13 @@ RUN curl --silent --show-error --location --output /usr/local/bin/cloud_sql_prox
chmod a=rx /usr/local/bin/terragrunt && \
# AquaSec Trivy for scanning container images for security issues
curl --silent --show-error --location "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" | \
tar --extract --gzip --directory=/usr/local/bin --file=- trivy
tar --extract --gzip --directory=/usr/local/bin --file=- trivy && \
# Anchore Syft for SBOM generation
curl --silent --show-error --location "https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/syft_${SYFT_VERSION}_linux_amd64.tar.gz" | \
tar --extract --gzip --directory=/usr/local/bin --file=- syft && \
# Sigstore Cosign for artifact signing and attestation
curl --silent --show-error --location --output /usr/local/bin/cosign "https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64" && \
chmod a=rx /usr/local/bin/cosign
# We use yq during "make deploy" to manually substitute out fields in
# our helm values.yaml file. See https://github.com/helm/helm/issues/3141

View File

@ -113,6 +113,7 @@
bat
cairo
curl
cosign
delve
dive
drpc.defaultPackage.${system}
@ -161,6 +162,7 @@
shellcheck
(pinnedPkgs.shfmt)
sqlc
syft
unstablePkgs.terraform
typos
which

View File

@ -153,4 +153,17 @@ if [[ "$push" == 1 ]]; then
docker push "$image_tag" 1>&2
fi
log "--- Generating SBOM for Docker image ($image_tag)"
syft "$image_tag" -o spdx-json >"${image_tag}.spdx.json"
if [[ "$push" == 1 ]]; then
log "--- Attesting SBOM to Docker image for $arch ($image_tag)"
COSIGN_EXPERIMENTAL=1 cosign clean "$image_tag"
COSIGN_EXPERIMENTAL=1 cosign attest --type spdxjson \
--predicate "${image_tag}.spdx.json" \
--yes \
"$image_tag"
fi
echo "$image_tag"