Lesson 05 of 8 · Modules
Supply chain: sign, attest, scan, verify
Put supply-chain security into the pipeline: build once, produce an SBOM and provenance, scan with policy, sign with keyless cosign or GitHub artifact attestations, and verify signatures before anything runs, always by digest.
The chain you're protecting
source ─► build ─► artifact (image) ─► registry ─► deploy ─► run
│ │ │ │ │
reviews provenance SBOM + scan signature verify
Attackers target every link: a compromised dependency, a tampered build, a swapped image in the registry, a mutable tag moved to something else. SLSA (Supply-chain Levels for Software Artifacts) describes increasing guarantees about the build; the practical steps below cover most of it.
Think of a sealed juice bottle. The ingredients label is the SBOM. The factory stamp saying where and how it was made is the provenance. The safety seal is the signature. The shop checks the seal before putting it on the shelf (verify at admission). A juice with a broken seal never reaches a customer.
The pipeline
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # keyless signing + attestations
attestations: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
- id: build
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/acme/api:${{ github.sha }}
- uses: sigstore/cosign-installer@v3
- uses: anchore/sbom-action@v0
with:
image: ghcr.io/acme/api@${{ steps.build.outputs.digest }}
format: spdx-json
output-file: sbom.json
- name: Scan (fail on critical)
env:
IMAGE: ghcr.io/acme/api@${{ steps.build.outputs.digest }}
run: |
# trivy installed beforehand from its official release (checksum verified) or baked into the runner image
trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed "$IMAGE"
- name: Sign and attest
env:
IMAGE: ghcr.io/acme/api@${{ steps.build.outputs.digest }}
run: |
cosign sign --yes "$IMAGE"
cosign attest --yes --type spdxjson --predicate sbom.json "$IMAGE"
- uses: actions/attest-build-provenance@v2
with:
subject-name: ghcr.io/acme/api
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
Everything targets the digest, never a tag. Pick one signing approach for provenance (cosign attestations or GitHub artifact attestations) and verify the same way everywhere; the example shows both for learning.
Verifying
$ cosign verify ghcr.io/acme/api@sha256:… \
--certificate-identity-regexp '^https://github.com/acme/api/\.github/workflows/release\.yml@refs/heads/main$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
$ gh attestation verify oci://ghcr.io/acme/api@sha256:… --owner acme
Then enforce it in the cluster with a policy engine (Kyverno verifyImages, Sigstore policy-controller, or similar), as shown in Kubernetes Security & Hardening, lesson 12. A keyless policy checks the identity (repo and workflow) and issuer, not a key.
Scenario: the rollback that redeployed the bug
Release 1.8 had a bug. The team "rolled back" by redeploying tag 1.7. The bug stayed. Someone had rebuilt and re-pushed 1.7 the week before (with a dependency update that introduced the bug), so the tag now pointed to different bytes.
What would have prevented it?
- Deploy by digest (
image@sha256:…) in manifests, so a rollback restores exactly the bytes that ran before. - Make release tags immutable in the registry (most registries support tag immutability rules).
- Keep the deployed digest in Git (GitOps), so
git revertreturns the exact previous digest. - Build once and promote the same digest; never rebuild a released version.
Try it: a signed, verifiable image (public repo or sandbox org)
- Add the release job to a small repo with a Dockerfile, and run it.
- Verify the signature locally with
cosign verifyand the identity regexp; then try a wrong identity and read the failure. - Download and inspect the SBOM attestation (
cosign download attestation … | jq), and list the packages. - Add a known-vulnerable base image and watch the scan fail the job.
- In a kind cluster with Kyverno, add a verifyImages policy for your identity; deploy the signed digest (allowed) and an unsigned image (refused).
Going deeper: supply-chain maturity
- Pin third-party actions to full commit SHAs: in 2025 a widely used action (
tj-actions/changed-files) was compromised and its version tags re-pointed to malicious code. Use Dependabot or Renovate to update pinned SHAs. - Use minimal base images and rebuild regularly; most vulnerabilities come from the base.
- Manage scan findings with policy (severity, fix available, exceptions with expiry), not endless red dashboards.
- Consider reusable workflows as trusted builders: verifiers can require that provenance came from your central build workflow.
Recap
- Protect every link: source → build → artifact → registry → deploy.
- In CI: build once, SBOM, scan with a policy gate, sign (keyless cosign) and attest provenance, all by digest.
- Verify at admission: identity + issuer for keyless signatures.
- Deploy digests, make release tags immutable, and never rebuild a released version.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.