The Admission Gate That Lists 320 KB Per Pod

A Kyverno policy that blocks Pods on CRITICAL CVE counts has to LIST every VulnerabilityReport in the namespace on every admission — 319,978 bytes for 15 reports. And the unscanned case, which should return zero, errors instead.

The Admission Gate That Lists 320 KB Per Pod

Blocking a Pod because its image has too many CRITICAL CVEs sounds like a one-line policy. Kyverno can read any API path through context.apiCall, Trivy Operator already publishes VulnerabilityReport CRDs, and the report body carries report.summary.criticalCount. Wire the two together and you have a gate.

The mechanism works. It is what the mechanism costs, and what it does when the data is missing, that decides whether you ship it.

What every admission fetches

The policy's apiCall is a namespace-scoped LIST:

/apis/aquasecurity.github.io/v1alpha1/namespaces/{{ request.namespace }}/vulnerabilityreports

There is no field selector and no server-side filter, because the match is on report.artifact.digest — a field inside the report body. So the API server returns the whole collection and Kyverno's JMESPath engine filters it in memory. Measured against the real CRDs in one POC namespace:

--- bytes returned by that LIST (this is what every Pod admission would fetch):
319978
--- number of reports in the LIST:
15

Roughly 320 KB, for 15 reports, in one namespace, on every single Pod admission in that namespace. And the policy issues this LIST twice per container — once for reportcount, once for criticalcount — because they are separate context entries.

One Pod admission triggers a full namespace LIST, twice per container
One Pod admission triggers a full namespace LIST, twice per container

The gate reads 319,978 bytes of VulnerabilityReports to answer one question about one digest.

Fifteen reports is a small namespace. The number scales with workloads per namespace, not with the Pod being admitted, so the cost of admitting an unchanged Pod grows as its neighbours multiply. That is why the policy carries webhookConfiguration.failurePolicy: Ignore — an unbounded LIST on the admission hot path must never be able to wedge the cluster.

The zero case errors instead of returning zero

Querying real reports returns what you expect: a python-3.11-slim fixture reports 1 report and 4 criticals, a Harbor Photon image 1 report and 2 criticals, a Dependency-Track frontend 1 report and 0 criticals. Then the digest that has never been scanned:

--- no-such-image-EXPECT-none  (sha256:0000...0000)
    reportcount   = 0
    criticalcount = Error: error evaluating JMESPath expression:
                    JMESPath function 'sum': at least one element in the
                    array is required

sum() over an empty array is an error, not zero. This is the single most consequential detail in the whole policy, because the empty array is the brand-new-image case — precisely the Pod you most want the gate to reason about. Without a guard, the context fails, the rule errors at admission, and the behaviour then depends entirely on your failure policy.

The fix is a zero-guard that appends a literal 0 before summing:

sum([items[?report.artifact.digest=='{{ element.digest }}'].report.summary.criticalCount, [`0`]][])

Now the empty case sums a one-element array and returns 0.

Why the unscanned case needs its own rule

With the guard in place, a never-scanned image looks identical to a clean image: both report zero criticals. So the threshold rule deliberately fails open — it only judges images where reportcount is greater than zero — and a second rule matches the reportcount == 0 case separately, with its own message: No Trivy Operator VulnerabilityReport exists yet for this image digest.

That separation exists because of a chicken-and-egg problem no JMESPath expression can solve: Trivy Operator only writes a report for a workload that has already been admitted. At admission time, a genuinely new image has no report. The gate can flag that, but it cannot close it.

Next: zero of 17 Pods pin a digest — a read-only measurement against the real cluster, and the three-rule policy it argues for.