You Cannot Phase In a Signature Policy

Every other Kyverno policy can be trialled in Audit mode first. Image-signature verification cannot — Kyverno 1.17.2 refuses mutateDigest:true together with failureAction:Audit, and rejects the policy outright.

You Cannot Phase In a Signature Policy

The standard way to introduce an admission policy is to run it in Audit first: let it report violations for a few weeks, find out who it would have broken, then flip it to Enforce once the noise is gone. That playbook works for registry restrictions, digest pinning, non-root — and it does not work for cosign image verification. Kyverno rejects the combination at validation time.

The error

kyverno apply kyverno/04-verify-image-signature-cosign.yaml \
  --resource kyverno-tests/res-cosign.yaml -v 4
spec.rules[0].verifyImages[0].mutateDigest: Invalid value: true:
  mutateDigest must be set to false for 'Audit' failure action

Not a warning. The policy never loads:

Applying 6 policy rule(s) to 3 resource(s)...
Policies Skipped (as required variables are not provided by the user):
1. poc-verify-image-signature

pass: 0, fail: 0, warn: 0, error: 1, skip: 0
EXIT_CODE=1

One error, zero evaluations, exit 1. Kyverno 1.17.2 treats this as a malformed policy rather than an unsupported runtime combination, which is the right call and also the inconvenient one.

Audit and mutateDigest are mutually exclusive, so the phase-in has a hole
Audit and mutateDigest are mutually exclusive, so the phase-in has a hole

Audit mode gets you the verification signal but not the digest pin; Enforce gets you both, with no intermediate step between them.

Why the two cannot coexist

mutateDigest: true rewrites the image reference from the tag to the digest that was actually verified. That is not a validation — it is a mutation, and it puts the policy in the mutating webhook path. A mutation that only sometimes applies, depending on whether verification passed, is not a coherent thing to ask of an admission chain: the Pod would be admitted either way, but with a different spec depending on a check that was explicitly declared non-blocking. Kyverno refuses to let you express it.

So the phase-in you actually get is a choice between two half-measures:

  • Audit with mutateDigest: false — you learn which workloads would fail verification, but nothing gets pinned, so the tag can still move between the check and the pull.
  • Enforce with mutateDigest: true — the target state, with no rehearsal period in front of it.

What the Audit variant does tell you

The mutateDigest: false Audit run against placeholder .invalid registries returns a different failure than the one you might expect:

1 - verify-key-based missing digest for harbor.poc.invalid/poc/tc01:v1
pass: 0, fail: 2, warn: 0, error: 2, skip: 0

"Missing digest" — because with mutation off and the registry unresolvable, Kyverno cannot turn the tag into a digest to verify against. The Enforce variant with mutateDigest: true fails on the same two Pods and reports four errors rather than two. Both runs exit 1. The useful signal from Audit mode is therefore narrower than it looks: it tells you which images Kyverno cannot resolve or verify, not what the enforcing policy would ultimately do to them.

The operational consequence

Signature verification has to be introduced by scope, not by severity. You cannot soften the policy; you can only narrow what it matches — one namespace, one image-reference glob, one team — and then widen it. The policy in the POC is scoped with imageReferences globs and a namespace match for exactly that reason, and it carries webhookConfiguration.failurePolicy: Ignore so a registry outage does not wedge Pod creation cluster-wide.

That is a different rollout shape from every other policy in the set, and it needs to be planned as one. Audit mode is not available to you here.

Next: cosign 3.1.3 removed offline signing — the version bump that quietly broke air-gapped signing.