mirror of
https://github.com/coder/coder.git
synced 2025-03-14 10:09:57 +00:00
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:
146
.github/workflows/ci.yaml
vendored
146
.github/workflows/ci.yaml
vendored
@ -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
|
||||
|
167
.github/workflows/release.yaml
vendored
167
.github/workflows/release.yaml
vendored
@ -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)"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
Reference in New Issue
Block a user