Zion Boggan
repos/cicd-supply-chain-security

cicd-supply-chain-security

This picks up where the secure CI/CD pipeline leaves off. That repo proves the *source* is clean. This one proves the *artifact* is - that the container image a cluster is about to run was built by my pipeline, hasn't been tampered with since, and ships with a verifiable bill ...

7 commits First commit May 1, 2026 Last commit Jun 22, 2026 (43 minutes ago)
YAML 55.0%Markdown 32.9%Shell 7.3%Python 4.8%
Files 13 entries
README.md

CI/CD Supply Chain Security

This picks up where the secure CI/CD pipeline leaves off. That repo proves the source is clean. This one proves the artifact is - that the container image a cluster is about to run was built by my pipeline, hasn't been tampered with since, and ships with a verifiable bill of materials.

The mechanism is Sigstore. The pipeline signs every image keylessly with Cosign using the workflow's OIDC identity, attaches an SBOM as a signed attestation, and a Kyverno policy refuses to admit anything to the cluster that can't produce both.

flowchart LR
    build[build image] --> sbom[syft SBOM]
    sbom --> scan[grype scan<br/>fail on high]
    scan --> sign[cosign sign<br/>keyless OIDC]
    sign --> attest[cosign attest<br/>SBOM]
    attest --> verify[cosign verify<br/>in-pipeline]
    verify --> admit[Kyverno admission<br/>at deploy time]

Keyless signing

There is no private key to manage or leak. Cosign requests a short-lived certificate from Fulcio bound to the GitHub Actions OIDC identity (https://github.com/<owner>/<repo>), signs, and logs the signature to the Rekor transparency log. Verification checks the certificate identity and issuer rather than a key you have to rotate.

That is why the workflow asks for id-token: write - without it there's no OIDC token to exchange for the signing certificate.

Cosign signing and verification, and what happens the moment a signed artifact is modified - the signature is rejected:

Cosign sign and verify

The screenshot uses a local key pair to show the sign/verify mechanics offline; the pipeline itself signs keylessly with the GitHub OIDC identity (no stored key).

What the pipeline does

Stage Tool Output
build buildx + build-push-action image pushed to GHCR by digest, with provenance + SBOM attestations
scan syft + grype SPDX SBOM artifact; build fails on a high/critical CVE
sign cosign keyless signature in Rekor
attest cosign signed SPDX attestation attached to the image
verify cosign signature + attestation checked before the run is called good

Everything operates on the image digest, never a mutable tag, so the thing that gets signed and verified is exactly the thing that was built.

Enforcing it at deploy time

policy/kyverno-verify-images.yaml is a ClusterPolicy in Enforce mode that:

  1. requires a Cosign signature from this repo's GitHub identity, and
  2. requires an SPDX SBOM attestation from the same identity,

for any image under ghcr.io/zionboggan/*. It also rewrites tags to digests on admission (mutateDigest: true) so a pod can't be pinned to a tag that later moves.

Apply it:

kubectl apply -f policy/kyverno-verify-images.yaml

A signed image from the pipeline is admitted; an arbitrary nginx:latest is rejected. policy/test/pods.yaml has one of each for kyverno apply to test against, which is what the admission-policy-check workflow runs on PRs that touch the policy.

Verify an image by hand

OWNER=zionboggan ./policy/verify.sh ghcr.io/zionboggan/cicd-supply-chain-security@sha256:<digest>

Notes

Keyless is the right default for CI - nothing to store, everything in the transparency log. If you needed an air-gapped or offline verifier you'd switch to a key pair (or a KMS key) and Cosign supports that, but for a GitHub-hosted pipeline the OIDC identity is the cleaner trust anchor. More in docs/supply-chain.md.