SBOM, a vulnerability scan with a provable blocking gate, keyless signing, and a cluster that rejects unhardened pods — plus the honest story of fighting a Critical CVE I couldn't bump away.
"We do DevSecOps" usually means a scanner runs somewhere and emails a report nobody opens. A control that can't stop a bad artifact isn't a control, it's a dashboard. So I set out to wrap four real supply-chain controls around the small service from the earlier golden-path work, each able to fail the build or block a deploy: an SBOM, a vulnerability scan with a gate that provably blocks, keyless signing, and a cluster policy that rejects anything unhardened.
Then the scanner immediately caught something I couldn't make go away, and that turned out to be the most useful part of the whole exercise. I'll get to it.
Everything runs on free infrastructure, so you can clone it and watch the controls reject things yourself. Repo: github.com/PradeepKandepaneni/golden-path-supplychain. This is the third piece in a series — first we shipped the service through a golden path, then we made it survive an availability-zone outage. Now the supply chain.
The four controls
An SBOM, because you can't secure what you can't enumerate. A Software Bill of Materials lists every component in the image. The pipeline generates one with syft in SPDX JSON. On its own it secures nothing, it's an inventory, but every control downstream leans on it, and when a CVE drops six months from now, the SBOM is what answers "are we affected?" in seconds.
A scan with a gate that provably blocks. The pipeline runs grype. More on exactly how the gate is wired below, because this is where the honest story lives.
Keyless signing, so there's no key to leak. The pipeline signs the SBOM with cosign using the workflow's OIDC identity, no private key stored anywhere. Traditional signing trades unsigned artifacts for a signing key that becomes the thing worth stealing. Keyless sidesteps that: the signature ties to the build identity, verifiable later, with nothing sitting in a secret store.
Policy as code, so hardening is enforced, not suggested. In the earlier pieces, the pod's hardening lived in a YAML file a tired engineer could quietly weaken. Here a kyverno cluster policy makes it an admission rule: the cluster refuses to create a pod that runs as root, keeps Linux capabilities, or uses :latest.
What the scanner actually caught
Here's the part I didn't plan. My first version of the gate was the obvious one: fail the build on any High or Critical CVE in the image. I pushed it, and it went red immediately. Grype had found a vulnerability at High severity or above, and the build refused to ship.
My first instinct was that it was the Go standard library compiled into the binary, which is the usual culprit for a minimal image. So I bumped the builder from Go 1.22 to 1.23. Still red. Bumped to the latest Go. Still red. Then the gate reported the finding as Critical, and it survived every toolchain bump I threw at it. That was the tell: it wasn't the Go binary at all. It was baked into the distroless base image, and no amount of rebuilding my own code would ever clear it.
This is the moment that teaches you something a happy-path tutorial never will. You hit a vulnerability you did not introduce, cannot fix by changing your code, and may not have a patched base image available for yet. Blocking the build forever on it accomplishes nothing except training the team to disable the scanner. And a disabled scanner is worse than no scanner, because it comes with a false sense of safety.
So the gate I actually shipped is tiered, and it mirrors what mature teams really do:
Scan the image and report its full posture, every finding, on every build. Nothing is hidden. The SBOM and the grype table are both published as artifacts.
Prove the blocking gate works by firing it against a deliberately vulnerable image. The pipeline scans an intentionally ancient image and fails the build unless grype blocks it:
yaml
- name: Prove the gate BLOCKS a known-vulnerable image run: | if grype python:3.4-alpine --fail-on high -q; then echo "FAIL: the gate did not fire on a known-vulnerable image." exit 1 fi echo "Gate correctly blocked a known-vulnerable image."
This separates two questions that the naive gate jams together: does the blocking mechanism work (proven, every run, against a known-bad image) and what is incidentally in my base image today (reported, tracked, re-scanned on a schedule as patches land). The control still has teeth. It just doesn't hold your merges hostage to a CVE in someone else's base layer that you can't fix this afternoon.
I want to be honest that this is a weaker literal gate than "fail on every High." It is also the version that survives contact with reality. The strongest-sounding policy is often the one teams quietly route around; the one that lasts is the one engineers can actually live with.
Proving the policy isn't theatre, either
The same philosophy drives the Kyverno step. It's easy to write an admission policy and assume it works. The pipeline instead applies a deliberately terrible pod, root, all capabilities, nginx:latest, and fails the build unless Kyverno rejects it:
yaml
- name: Assert the BAD pod is REJECTED run: | if kubectl apply -f policy/bad-pod.yaml 2>err.txt; then echo "FAIL: cluster accepted an unhardened, :latest, root pod." exit 1 fi echo "Policy correctly rejected the bad pod."
Getting this green taught its own small lesson. The first run failed with connection refused on Kyverno's webhook, because the admission controller reported "rolled out" before its webhook endpoint was actually serving. The fix is the unglamorous reality of admission webhooks in CI: wait for every controller to be available, give the endpoints a moment to register, and retry the policy apply instead of trusting the first attempt. A policy you've only ever watched approve things is a policy you're trusting on faith. Watching it reject the bad pod, on every push, is the difference.
Every bit of this runs free: public-repo Actions minutes are unlimited, and the Kyverno cluster spins up in the runner via kind.
What this does not solve
Scanners only know what's in their database. A clean scan means "no known vulnerabilities, today." Tomorrow's disclosure flips that with no change to your code, which is exactly why reporting and re-scanning on a schedule matters more than a single build-time pass/fail.
Signing proves provenance, not quality. Keyless signing tells you who built the artifact and that it wasn't tampered with. It does not tell you the thing they built is good.
Policy enforces posture, not intent. Kyverno can require non-root with no capabilities. It cannot tell a well-behaved workload from a malicious one that happens to satisfy those rules.
None of that makes the controls pointless. It makes them layers, each catching what the others miss, none complete alone.
Takeaways
A control's value is in whether it can say no, not in how loudly it fails. The point isn't "fail on everything." It's having a blocking mechanism you've proven works, pointed at the things you can actually act on.
Prove your gates reject something. Don't just write the rule, fire it, against a known-bad image, against a bad pod, on every push. An unexercised gate is a guess.
Design for the vulnerability you can't fix today. The naive "block on every High" policy dies the first time a base image ships an unfixable Critical. Report everything, block on what's actionable, and re-scan as patches land. That's the version a team keeps instead of disabling.
The repo is public and runs for $0: github.com/PradeepKandepaneni/golden-path-supplychain. Clone it, and try to sneak a root container past the cluster.
Top comments (0)