A Policy Bundle With One Wrong Document Is a Silent No-Op

A policy file carrying its companion ClusterRole made `kyverno apply` report 'Applying 0 policy rule(s)' and exit 0 — a green CI gate enforcing nothing. The fix is to assert the rule count, not the exit code.

A Policy Bundle With One Wrong Document Is a Silent No-Op

The vulnerability-report policy needs a companion ClusterRole — Kyverno cannot read aquasecurity.github.io CRDs without one. The natural thing to do is keep them in the same YAML file, two documents separated by ---, so the policy and its permissions travel together.

Do that and kyverno apply stops applying the policy. Quietly.

The output that means nothing happened

Applying 0 policy rule(s) to 4 resource(s)...
pass: 0, fail: 0, warn: 0, error: 0, skip: 0
EXIT_CODE=0

Zero rules. Zero resources judged. Exit 0. There is no error, no warning, no "skipped 1 document" line — a bundle containing one non-Kyverno document is silently skipped, and what remains is a run that enforced nothing and announced success.

Split the ClusterRole into its own file and the same command against the same fixtures produces:

Applying 6 policy rule(s) to 4 resource(s)...
pass: 4, fail: 4, warn: 0, error: 0, skip: 0
EXIT_CODE=1

Six rules, four denials, exit 1. Same policy text, same Pods, opposite verdict — the only difference is which file the ClusterRole lives in.

A two-document bundle producing zero rules and exit 0, versus the split files producing six rules and exit 1
A two-document bundle producing zero rules and exit 0, versus the split files producing six rules and exit 1

The same policy, the same fixtures: one extra document in the file turns the gate into a no-op that reports success.

Why the exit code cannot catch it

A CI gate built on kyverno apply asks one question: did anything fail? When nothing was evaluated, nothing failed, so the answer is no, so the job is green. The failure mode and the success signal are indistinguishable at the exit code. This is the same shape as the kyverno test asymmetry from yesterday's post, arriving through a different door: the tool is not lying, it is answering a question that stopped being the one you meant to ask.

And it is easy to trip. Bundling a policy with its RBAC is good GitOps hygiene — one file, one unit of review, no chance of applying the policy without the permissions it needs. The practice that makes the cluster manifest correct is exactly the practice that makes the CI gate inert.

The guard

Assert the rule count in the pipeline. The number is knowable — you wrote the policy, you know how many rules it has, autogen included — so pin it:

kyverno apply policies/ --resource fixtures/ 2>&1 | tee out.txt
grep -q "Applying 6 policy rule(s)" out.txt || { echo "rule count changed"; exit 1; }

That one line catches the no-op, and it also catches the neighbouring failures: a policy file that failed to load, a rule renamed out of the match block, an autogen change that silently halved the surface area. None of those move the exit code either.

The general rule for any gate whose verdict is "nothing was wrong": make it prove it looked. Exit 0 from an evaluator means no violations found, which is a different claim from this policy is enforced, and the gap between them is where a green pipeline hides an empty one.

Next: native ValidatingAdmissionPolicy vs Kyverno — the same two controls, evaluated both ways, and what CEL's error messages give you for free.