CI/CD & Software Supply Chain›05 · Supply chain: sign, attest, scan, verify
Learning Hub / Delivery & Infrastructure as Code / CI/CD & Software Supply Chain

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.

Advanced
Key wordssoftware supply chainSLSAprovenanceSBOMvulnerability scanningcosign keylessSigstoreartifact attestationsdigestsverify at deploy
Source reviewed commit Build CI, pinned deps SBOM + scan syft, trivy Sign + attest cosign Registry by digest Admission verify signature Deploy only what you can prove: who built it, from which source, containing what attackers target every link; each step adds evidence the next one checks
Build once, then SBOM, scan, sign and attest, and verify at admission.

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 revert returns 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)

  1. Add the release job to a small repo with a Dockerfile, and run it.
  2. Verify the signature locally with cosign verify and the identity regexp; then try a wrong identity and read the failure.
  3. Download and inspect the SBOM attestation (cosign download attestation … | jq), and list the packages.
  4. Add a known-vulnerable base image and watch the scan fail the job.
  5. 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.