ProvenancePipeline is finished: the unsigned image never became a pod
The unsigned image never reached runtime.
There was no container to inspect, no Falco process event, and no NetworkPolicy decision. Kyverno rejected the request while the Kubernetes API was still deciding whether the pod should exist.
That denial completes ProvenancePipeline.
The positive test matters too. The signed image from the expected GitHub release workflow passed the same policy and started normally in KateClusters. The admission rule can distinguish the trusted artifact from the untrusted one without blocking the whole deployment path.
What the completed path looks like
One source commit now leads to one verifiable image digest.
The application tests run first. The container is built from digest-pinned bases and produces a distroless, non-root runtime image. Syft records the components in SPDX and CycloneDX SBOMs. Grype evaluates the artifact and blocks CRITICAL findings.
Cosign signs the digest through the GitHub Actions OIDC identity. The signing event receives a transparency record. The workflow attaches the SBOM and SLSA provenance as attestations.
A separate verification job starts without the build job's workspace and checks the published artifact. It verifies the digest, exact workflow identity, issuer, signature, SBOM attestation, and provenance. It also proves that an incorrect identity is rejected.
Kyverno then repeats the important trust decision at admission time.
CI produces the evidence. The cluster requires it.
Installing Kyverno was not the interesting part
The installation became healthy without much drama.
The policy behavior took longer.
I first applied the image verification policy in audit mode. That let me confirm which workloads matched, what image reference Kyverno evaluated, and whether the registry material was available from the admission path.
The initial image scope needed tightening. A policy that targets too broad a pattern can break system workloads unrelated to the project. A pattern that is too narrow creates an obvious bypass through another repository path.
Once the match behavior was clear, I moved the policy to enforcement and set the failure behavior to closed.
If the verification service cannot establish trust, the workload does not enter.
That choice trades availability for integrity at this boundary. It is deliberate, and it needs operational support. A broken registry or verification path can now stop a deployment even when the application image itself is valid.
Fail closed is not free security. It is a failure mode I have chosen to own.
The allowed deployment
The positive test used the exact digest produced by the release workflow.
Kyverno found the signature and attestations, validated the expected issuer, and matched the certificate identity for the release workflow on the main branch.
The pod was admitted.
After admission, the existing KateClusters controls still applied. The workload ran as non-root, used the intended Service Account behavior, and remained within the namespace's network policy.
Supply-chain verification did not replace runtime security. It answered whether the cluster recognized the artifact's origin. KateClusters still controlled what the admitted process could do.
The denied deployment
The negative image used the same kind of valid container reference but lacked the required signature and provenance from the trusted workflow.
The manifest passed ordinary schema validation. The registry reference resolved. Admission failed at image verification.
No pod object was created.
I preserved the denial output and the relevant Kyverno policy result. The evidence records the policy, tested image reference, time, namespace, and observed decision without copying unrelated credentials or registry tokens.
I repeated the test with the policy already warmed and the controllers healthy. The denial was not a startup artifact.
The result is simple to describe:
-
Signed by the expected workflow with the required evidence: admitted.
-
Missing the required trust evidence: denied.
-
Verification unavailable: denied under the configured failure policy.
That is the boundary I set out to build.
The wrong identity still fails
The pipeline's negative identity check remains important after admission enforcement.
An image can be signed and still be signed by the wrong party. "Signed" is not equivalent to "trusted."
The expected issuer is GitHub Actions. The expected identity is the exact release workflow and branch context defined by the project.
The clean verification job rejects the wrong identity. The Kyverno policy is based on the same trust anchor rather than accepting any valid Sigstore certificate.
This closes a tempting shortcut. A signature proves possession of some signing identity. Policy decides whether that identity belongs to the release path I trust.
What the SBOM and scanner do not do
The completed admission test does not make the SBOM a guarantee of safety.
The SBOM lists known components found in the artifact. Grype compares that inventory with vulnerability data available at scan time. A vulnerability can be unknown, incorrectly matched, or irrelevant to the way a component is used.
The current policy blocks CRITICAL findings and reports lower severities. Another environment might choose a different threshold, add exploitability context, or use reviewed exceptions.
I kept the project policy visible and simple. I did not weaken it to make a release pass, and I did not describe it as complete vulnerability management.
The SLSA claim remains Level 2
Finishing the project did not change the builder model.
The provenance is authenticated and tied to the artifact, but the build workflow has more influence over its own evidence than a Level 3 design should allow. A compromised trusted job could still produce a malicious artifact and matching provenance under the expected identity.
The next architectural improvement would be an isolated builder designed for the stronger level.
Until that exists, the honest claim is SLSA Build Level 2.
The project is finished with that limitation. Completion does not require renaming the limit.
What I would operate carefully
Admission verification adds dependencies to every matching deployment.
I would monitor Kyverno availability, policy evaluation errors, registry latency, signature retrieval failures, and changes to the trusted workflow identity. I would also test policy updates with the same care as application releases because a bad policy can block every workload in scope.
Break-glass access would need strict ownership, logging, and a time limit. A permanent exclusion annotation would turn enforcement into a suggestion.
The personal lab demonstrates the control path. A production version would need an operational path for outages and certificate or workflow changes without silently switching to allow.
ProvenancePipeline is done
The repository now contains the small Go application, tests, Dockerfile, SBOM generation, vulnerability gate, keyless signing, transparency evidence, SLSA provenance, independent verification, negative identity control, Kyverno policy, and allow and deny evidence.
More important, the final repository can answer one question from source to cluster:
Why was this exact image allowed to run?
It can point to a commit, workflow, digest, component inventory, scan, signature, issuer, identity, provenance record, and admission result.
For the unsigned image, the answer is shorter.
It was not allowed to run.