Proving the artifact, not just the source
A clean source scan and a build step are not a chain of custody. Here is the supply-chain layer that closes the gap, with no private key to lose.
A clean source scan and a build step are not a chain of custody. Here is the supply-chain layer that closes the gap, with no private key to lose.
My secure CI pipeline proves the source is clean. For a while I thought that was the finish line. Then I sat with the actual question a cluster has to answer at admission time, which is not is this source clean. It is should I run this exact image. Those are different, and the gap between them is where a supply-chain attack lives.
Anything that can push to your registry can substitute an image between the build that scanned clean and the deploy that runs it. The scan happened on one artifact. The cluster runs another. Nothing in the source-side pipeline notices, because the source-side pipeline already finished. So I built the layer that proves the artifact itself.
The mechanism is Sigstore. The pipeline signs every image keylessly with Cosign using the workflow's OIDC identity, attaches an SPDX SBOM as a signed attestation, and a Kyverno policy refuses to admit anything to the cluster that cannot produce both.
Keyless was the decision I deliberated over and have not regretted. There is no private key to store, rotate, or recover. The trust anchor is the GitHub OIDC identity that already exists for the workflow. Cosign requests a short-lived certificate from Fulcio bound to that identity, signs the image digest, and logs the signature to the Rekor transparency log. A long-lived signing key is one more secret to leak, and a leaked signing key is a quiet catastrophe. Not having one is simpler and safer at the same time, which is rare.
Everything operates on the image digest, not a mutable tag. A tag can be moved after signing. A digest cannot. The thing that gets signed is exactly the thing that was built, and that exactness is the entire value of the exercise. If I signed a tag, an attacker could move the tag to a different image and my signature would happily vouch for the wrong thing.
The pipeline runs four jobs after build: scan with syft and grype, sign with Cosign, attest the SBOM, and verify both in-pipeline before the run is marked successful. Verifying in the pipeline is a sanity check on my own signing.
The enforcement that actually protects the cluster is the Kyverno ClusterPolicy in Enforce mode. A signed image from this pipeline is admitted. An arbitrary nginx:latest pulled from anywhere is rejected, because it cannot produce a signature from the expected OIDC identity or an SBOM attestation. The policy checks identity and issuer, not a key I have to manage. I keep one signed pod and one unsigned pod in the repo so kyverno apply can prove, on every PR that touches the policy, that the rule still admits the right thing and rejects the wrong thing.
Source scanning and artifact signing are two halves of one idea: the cluster should only run software whose provenance it can check. Neither half is sufficient alone, and I learned that the hard way by building the first half and assuming I was done.