This is part four of Ship the Proof. The previous part made the evidence travel with a digest through promotion and verified it at the destination. This part puts the same check at the one place every deployment has to pass through, because a promotion gate protects a registry hop, not the cluster.

Your pipeline signs every image it builds. Good. Now answer this: what stops someone from applying a different, unsigned image straight into the cluster? If the answer is “nobody would do that” or “our deploy script always signs,” you have a convention, not a control. The signature your pipeline produced is sitting in a registry, and nothing is forcing the path into your cluster to consult it.

The invariant: a build-time claim is not a runtime guarantee

When CI signs an artifact, it asserts something true about that artifact at that moment: this digest was built this way, contained these components, passed these tests. With Sigstore’s keyless model the assertion is strong. Fulcio “issues short-lived certificates binding an ephemeral key to an OpenID Connect identity,” the private key “is destroyed shortly after,” and signing events are “logged in Rekor, a signature transparency log, providing an auditable record of when a signature was created” (Sigstore signing overview).

Notice what that assertion does not do. It does not stop someone from deploying a different, unsigned image. It does not stop an image whose attestations were stripped during a registry copy. It does not stop a stale, vulnerable digest that was signed months ago and never re-evaluated. The signature exists. Nothing in the deploy path is obligated to look at it.

If the only thing between “signed in CI” and “running in production” is a convention (a pipeline step everyone agrees to run, a checklist item, a well-behaved deploy script) then your guarantee is exactly as strong as the weakest path into the cluster. Any deploy that bypasses the pipeline bypasses the guarantee. A change made by hand during a 3 a.m. incident bypasses it. Convention is not enforcement.

The property: verify at the boundary every deployment must cross

There is exactly one place every deployment passes through, whether it is pipeline-driven or hand-typed, well-behaved or not: the admission boundary into the cluster. Enforce there and the guarantee stops depending on the deploy path being trusted, because there is no deploy path that skips admission.

State the property without naming a tool. At admission, a workload is allowed to run only if its image carries a valid signature and the required attestations, produced by a builder identity you trust. Anything else is refused. Two settings make that real, and they are the whole point:

  • Deny by default. A violation is a hard rejection, not a logged warning. An image that lacks a valid signature or a required attestation does not run.
  • Fail closed. If the check itself cannot be evaluated, because the verifier is unreachable or times out, the request is rejected rather than admitted.

The combination is what matters. Deny without fail-closed leaves a gap: an attacker who can knock the verifier offline gets a free pass, because an unevaluated request sails through. Deny plus fail-closed shuts it. A missing attestation is a rejection, and an unanswerable question is a rejection too.

One implementation, trimmed to the load-bearing fields

A Kubernetes admission policy is one way to satisfy the property. Kyverno’s ImageValidatingPolicy reached a stable v1 API in Kyverno 1.17, released 2 February 2026 (Kyverno 1.17). Stripped to the parts that carry the meaning:

spec:
  validationActions: [Deny]          # hard rejection, not a warning
  webhookConfiguration:
    failurePolicy: Fail              # unevaluable request is rejected, not admitted
  matchImageReferences:
    - glob: 'ghcr.io/myorg/*'
  attestors:
    - name: githubBuilder            # pin trust to a specific signing identity
      cosign:
        keyless:
          identities:
            - issuer: 'https://token.actions.githubusercontent.com'
              subject: 'https://github.com/myorg/myrepo/.github/workflows/build.yml@refs/heads/main'
  attestations:
    - name: slsaProvenance
      intoto:
        type: https://slsa.dev/provenance/v1

Two fields do the real work. validationActions: [Deny] and failurePolicy: Fail are the deny-by-default, fail-closed pair. Pinning the keyless subject to a specific workflow ref is what binds admission to a builder identity: only attestations produced by that exact workflow satisfy the policy. Use the literal predicate string https://slsa.dev/provenance/v1, because the SLSA spec instructs consumers to “always use the above string for predicateType rather than what is in the URL bar” (slsa.dev/provenance/v1).

This is not specific to one tool. Other admission verifiers implement the same shape: the Sigstore policy-controller admits an image only after it “has been validated against all ClusterImagePolicy that matched the digest,” rejecting matched-but-unsatisfied images by default (policy-controller). Pick whichever you run. The property is what matters, not the binary.

Roll it out audit first, then enforce

Do not turn on deny cold. You will block legitimate workloads you forgot about, an image from a registry that does not match your glob, a service still signed by an older workflow ref, a third-party sidecar, and you will spend the outage learning which.

Start in audit mode. Violations are logged without blocking pods, so you find the false negatives (correctly-signed images that fail to match for some reason you did not anticipate) without breaking anything. Let it soak across a representative deploy window, read the reports, and only when they are clean do you flip to deny with fail-closed. Then prove both directions: a signed, attested image is admitted, and an unsigned one is rejected. An admission control you have not watched reject something is a control you are only assuming works.

Why this is the right boundary

SLSA frames the whole exercise as adversary cost. Its build levels are graded by how hard forgery is. Build Level 2 means “forging the provenance or evading verification requires an explicit ‘attack’, though this may be easy to perform,” and Level 3 means it “requires exploiting a vulnerability that is beyond the capabilities of most adversaries” (SLSA v1.2 build-track-basics).

That cost only buys you something if someone actually verifies. The standard can tell you to verify, but it cannot force your cluster to. That last mile is yours. Build-time attestation establishes the claim. Admission-time enforcement, deny-by-default and fail-closed, is what makes the claim binding on what actually runs. Everything between those two points is convention, and convention is what an incident, a mistake, or an attacker routes around.

The next part backs up one step to the build itself, and shows that the assurance level admission depends on is cheaper to reach honestly than its reputation suggests.

Sources