Securing Your Container Supply Chain: SBOM, Cosign Signing, and SLSA Provenance in One Pipeline
Generate an SBOM, sign with cosign, attach SLSA provenance, and enforce with Kyverno. One GitHub Actions pipeline.

- Most container security guides stop at 'scan your image.' Supply chain attacks require four layers: SBOM generation, image signing, provenance attestation, and admission enforcement.
- Cosign keyless signing uses ephemeral OIDC-backed certificates. No long-lived private keys to rotate, leak, or manage in CI.
- SLSA Level 2 provenance is a signed attestation that records exactly which source commit and build system produced your image. It's automatable with slsa-github-generator in one workflow step.
- Kyverno policies can block any unsigned or unattested image at the Kubernetes admission layer, so even a compromised registry can't sneak an image into production.
- The 2024 Trivy-action incident (75 of 76 version tags poisoned via force-push) is exactly why tag-based pulls without signature verification are dangerous.
In April 2024, 75 of 76 version tags in the aquasecurity/trivy-action repository were force-pushed to a malicious commit. Anyone using a tag pin (rather than a commit SHA) silently pulled a tampered scanner. The attack didn't touch DockerHub. It exploited one simple fact: tags are mutable, and nobody was checking signatures.
That's the supply chain threat container teams face. Not a bug inside your app. A compromise of the build artifacts flowing into your cluster. A poisoned base image. A hijacked CI action. A registry mirror serving stale layers. CVE scanning is necessary. It's not enough. What you need is a chain of custody from source commit to running pod.
This guide builds that chain. By the end you'll have a GitHub Actions pipeline that does four things: generates a machine-readable SBOM, signs your image with cosign keyless (no long-lived secrets), attaches SLSA Level 2 provenance, and enforces all three at the Kubernetes admission layer with Kyverno. Every step uses open-source tooling. Each layer adds to the one before it.
You can adopt these layers incrementally. Each one independently reduces risk. Start with SBOM generation in CI, add signing next week, and layer in admission enforcement when you're ready. You don't need all four on day one.
Why four layers? What each one actually stops
Before writing any YAML, it's worth understanding what threat each layer addresses. They're different threats, and skipping one leaves a real gap.
| Layer | Tool | What it stops |
|---|---|---|
| SBOM generation | Syft or Trivy | Unknown composition: you can't triage a Log4Shell if you don't know who's shipping log4j-core in their image. |
| Image signing | cosign (keyless) | Tampered or substituted images: a signed image from your registry can be verified to come from your build, not an attacker's. |
| SLSA provenance | slsa-github-generator | Build-time compromise: provenance links the image digest to the exact source commit and build job, so you can detect a poisoned pipeline. |
| Admission enforcement | Kyverno | Runtime introduction of unsigned images: blocks pods that lack a valid signature or provenance attestation from starting in the cluster. |
What you'll need
- A GitHub repository with Actions enabled
- A container registry that supports OCI 1.1 artifacts (Docker Hub, GHCR, ECR, GCR, and ACR all work)
- A Kubernetes cluster (local kind/k3d works fine for testing) with Kyverno installed
- The cosign CLI installed locally for verification steps (`brew install cosign` / `go install github.com/sigstore/cosign/v2/cmd/cosign@latest`)
Layer 1: Generate an SBOM inside the container build
An SBOM (Software Bill of Materials) is a structured inventory of every package, library, and OS component inside your image. It's what lets you answer "are we affected by CVE-2021-44228?" in minutes rather than hours. The right place to generate it is immediately after the docker build step, before the image leaves CI.
We'll use Syft (from Anchore) to generate a CycloneDX 1.6 SBOM and push it as an OCI artifact attached to the image digest. This keeps the SBOM co-located with the image in your registry. No separate storage, no sync problem.
CycloneDX 1.6 is the format of choice here: it's the same format used for CBOM, AIBOM, and HBOM, making it easy to extend your container inventory into a full BOM posture later. SPDX is a valid alternative if your toolchain already produces it.
Add this to your GitHub Actions workflow after the `docker/build-push-action` step:
- Install Syft: `curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin`
- Generate the SBOM: `syft ${IMAGE}@${DIGEST} -o cyclonedx-json > sbom.cdx.json`
- Attach it to the image: `cosign attach sbom --sbom sbom.cdx.json --type cyclonedx ${IMAGE}@${DIGEST}`
- Optionally scan the SBOM for known CVEs with Grype: `grype sbom:sbom.cdx.json --fail-on high`
A note on using the image digest rather than the tag: the digest is immutable. Once you've pinned your SBOM to a digest, you know exactly which image layers it describes. Tags can be overwritten (see the trivy-action incident). Throughout this guide we work with digests.
Layer 2: Sign the image with cosign keyless
Image signing proves that an image came from your pipeline and hasn't been tampered with. The old way: store a long-lived private key as a CI secret, rotate it, scan for leaks, repeat. It works. It's also annoying.
Cosign keyless signing removes long-lived keys entirely. GitHub Actions issues a short-lived OIDC token for each workflow run. Cosign uses that token to get an ephemeral certificate from Sigstore's Fulcio CA. The signature lands in Sigstore's Rekor transparency log. To verify, you check the log. No private key to manage, rotate, or lose.
“Keyless signing doesn't mean 'no cryptography.' It means 'no long-lived secret to rotate, leak, or lose.' The private key exists for milliseconds inside the GitHub Actions runner.”
The GitHub Actions steps are straightforward once you understand the flow:
- Add `id-token: write` and `contents: read` to your workflow permissions (this is what lets Actions fetch an OIDC token)
- Install cosign: `uses: sigstore/cosign-installer@v3`
- Sign after push: `cosign sign --yes ${IMAGE}@${DIGEST}` (cosign fetches the OIDC token automatically in Actions)
- Verify locally: `cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp 'https://github.com/YOUR_ORG/YOUR_REPO' ${IMAGE}@${DIGEST}`
The verification command is the critical part: it checks that the certificate was issued by GitHub Actions for your specific repository. An attacker who compromises a different repository cannot produce a valid signature for yours.
Do NOT use `cosign sign` without `--yes` in automation. It prompts for user input and will hang. Also confirm your registry supports OCI referrers (needed to attach the signature). GHCR, ECR (with OCI 1.1 enabled), and GCR all do.
Layer 3: Attach SLSA Level 2 provenance
A cosign signature proves an image is authentic. SLSA provenance proves how it was built. That distinction matters. If your build pipeline gets compromised (a malicious build step injected, a GitHub Action hijacked) the image might be legitimately signed but built from something you didn't intend.
SLSA provenance is a signed attestation. It records the source repo, the commit SHA, the build system, and the environment that produced the artifact. At Level 2, it must be signed by the build service itself (GitHub Actions), not just your key. That closes the self-attestation gap.
The SLSA GitHub Generator project makes Level 2 provenance a single reusable workflow call:
- In your existing workflow, output the image digest as a job output: `echo "digest=$(docker inspect --format='{{index .RepoDigests 0}}' ${IMAGE} | cut -d'@' -f2)" >> $GITHUB_OUTPUT`
- Add a separate job that calls the reusable workflow: `uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]`
- Pass `image: ${{ env.IMAGE }}` and `digest: ${{ needs.build.outputs.digest }}` as inputs
- The generator job runs in a hermetic, attester environment and signs the provenance with its own OIDC identity, distinct from your build job
The result is a signed provenance attestation pushed to your registry alongside the image. You can verify it with `slsa-verifier verify-image --source-uri github.com/YOUR_ORG/YOUR_REPO ${IMAGE}@${DIGEST}`.
SLSA Level 3 (the maximum for containers) requires a fully isolated build environment. Level 2 (what slsa-github-generator provides) is the practical sweet spot for most teams: the provenance is signed by GitHub, not by you, which means it can't be self-issued by a compromised build job.
Layer 4: Enforce at admission with Kyverno
Signing and provenance only protect you if something actually checks them. A Kyverno ClusterPolicy running in your cluster can verify the cosign signature and SLSA attestation at admission time, before any pod is allowed to start. This closes the gap: even if someone pushes an unsigned image to your registry, it can't run in production.
Install Kyverno via Helm if you haven't already: `helm install kyverno kyverno/kyverno -n kyverno --create-namespace`. Then apply a policy that requires a valid cosign signature:
A minimal signature verification policy in Kyverno looks like this:
- Create a ClusterPolicy of kind `ClusterPolicy` with `validationFailureAction: Enforce`
- Under `rules`, add a `verifyImages` rule targeting your registry prefix (e.g. `ghcr.io/YOUR_ORG/*`)
- Set `attestors` to your GitHub Actions OIDC issuer and subject pattern (this is what ties the signature to your specific repository)
- Optionally add an `attestations` block requiring the SLSA provenance predicate type `https://slsa.dev/provenance/v1`
- Test by attempting to run an unsigned image. Kyverno should block it with a clear audit log entry
Start with `validationFailureAction: Audit` (logs without blocking) to understand what would be blocked before flipping to `Enforce`. This is especially important if you're rolling Kyverno into an existing cluster with a mix of signed and unsigned images.
Putting it together: the complete pipeline
Here's the structure of the complete GitHub Actions workflow. There are three jobs: `build` (build, push, generate SBOM, sign), `provenance` (SLSA attestation via the reusable workflow), and a `verify` job that runs as a gate before any deployment proceeds.
- `build` job: checkout → login to registry → build and push → get digest → generate SBOM with Syft → attach SBOM → sign image with cosign keyless → output digest
- `provenance` job: depends on `build` → calls `slsa-framework/slsa-github-generator` with the image and digest
- `deploy` job (optional): depends on both → runs `cosign verify` + `slsa-verifier verify-image` as a pre-deploy gate. Fail fast before any `kubectl apply`
Pin ALL your GitHub Actions to commit SHAs, not tags (e.g. `sigstore/cosign-installer@dd9d4d316e5d5b0acb5d9e5e3af82c82a4c5b7c9` not `@v3`). The trivy-action incident was a tag-based attack. Dependabot can keep your SHA pins current automatically.
SBOM-driven vulnerability triage
The SBOM you generate isn't just a compliance artifact. It's the foundation for prioritized vulnerability triage. Once every image has a machine-readable SBOM attached as an OCI artifact, your security tooling can continuously scan for new CVEs without re-pulling images. When Log4Shell dropped in December 2021, teams with SBOMs found every affected image in minutes; teams without them spent weeks running manual grep exercises.
The workflow is: new CVE published → your scanner matches it against the indexed SBOMs → affected images surfaced immediately with exact package version and all affected tags/digests. Pair this with reachability analysis (is the vulnerable function actually called?) to prioritize which findings need emergency patching vs. which can wait for the next release cycle.
Studies consistently show 60–80% of SCA/container CVE alerts are for packages not reachable in the application's execution path. Reachability analysis is what separates an actionable alert from noise.
Mapping to CERT-In and SEBI requirements
If you're operating in India under CERT-In's Technical Guidelines v2.0 (July 2025) or SEBI's cybersecurity circular, this pipeline directly addresses several requirements. CERT-In mandates SBOM for all software components including containerized applications. The provenance attestation addresses the supply chain integrity requirements. Image signing satisfies the artifact authenticity controls.
| Pipeline layer | CERT-In v2.0 requirement | SEBI relevance |
|---|---|---|
| SBOM generation | Mandatory for software components, 21 required fields including supplier, version, license | SEBI cybersecurity circular: software transparency for regulated entities |
| Image signing | Artifact integrity controls; audit trail of software provenance | Non-repudiation requirements for software delivery |
| SLSA provenance | Build integrity; tamper-evident build pipeline evidence | Applicable to tech infrastructure of market intermediaries |
| Admission enforcement | Runtime control; only authorized software executes | Runtime security controls for systemically important entities |
Common pitfalls
- **Signing the tag, not the digest.** Tags are mutable. Always sign and verify by digest (`@sha256:...`). cosign will warn you if you try to sign a tag.
- **Forgetting `id-token: write` permissions.** Without this, GitHub won't issue an OIDC token and keyless signing fails silently with a confusing auth error.
- **OCI referrers support.** Some private registries don't support OCI 1.1 referrers by default. Check that your registry accepts `cosign attach` before you build the pipeline.
- **Kyverno in Audit before Enforce.** Jumping straight to Enforce in production is how you cause an outage. Always run a 48-hour Audit window first to inventory unsigned images.
- **Not pinning the slsa-github-generator version.** Same logic as pinning any Action: use the specific release tag and ideally the commit SHA.