TT Lab
Get started
Learn Learning paths Courses

KCA — Kyverno Certified Associate

What match Cannot See, preconditions and context Can

Continue in TT Lab

In one line

match/exclude select only by outward shape such as kind, name, and label. To look inside a resource's spec, or to decide whether to apply a rule by comparing with other resources, you need preconditions and context. This article explains in what order the two are evaluated and how far they can go, following the preconditions section of the official documentation and the external data sources section.

Why this was needed

Suppose you write the policy "a NodePort Service must have externalTrafficPolicy: Local." The match block can select only as far as kinds: [Service] and cannot see whether spec.type is NodePort. So every Service gets caught by the rule to begin with, and a second gate is needed to pick out only the NodePort ones. That gate is preconditions.

The second requirement comes up even more often. "How many Pods already exist in this namespace," "the allowed registry list is in a ConfigMap and I want to compare against it," "I want to ask with a SubjectAccessReview whether the requester actually has permission." These judgments cannot be made from the request body (the AdmissionReview) alone, and you have to fetch data from somewhere else. That is context.

How it works

Evaluation order

The documentation states the order clearly. Preconditions are evaluated after the resource is caught by match and not dropped by exclude, and when the whole thing is TRUE, the rule body (validate, mutate, and so on) runs. You cannot use variables inside match/exclude — the variables documentation gives the reason as "to select rules quickly without loading data." Preconditions, on the other hand, can use variables, JMESPath, and operators in full.

They also differ in result reporting. A resource caught by exclude is ignored altogether, but a resource that is caught by match and fails at preconditions is scored as skip. If you see a lot of skip in a PolicyReport, it was filtered out by preconditions.

any and all

You put the expressions of preconditions under an any or all block. any is a logical OR and all is a logical AND, and you can also put both in one rule. As the documentation puts it, the rule proceeds only if "each any/all block as a whole is TRUE," and if even one is not TRUE, the rule is not applied. It has the same structure as the conditions of a deny rule and does short-circuit evaluation in the same way.

preconditions:
  any:
  - key: "{{ request.object.metadata.labels.color || '' }}"
    operator: Equals
    value: blue
  - key: "{{ request.object.metadata.labels.app || '' }}"
    operator: Equals
    value: busybox
  all:
  - key: "{{ request.object.metadata.labels.env || '' }}"
    operator: Equals
    value: qa

A single expression consists of key, operator, and value. The operators are Equals and NotEquals, GreaterThan, GreaterThanOrEquals, LessThan, and LessThanOrEquals, the set comparisons AnyIn, AllIn, AnyNotIn, and AllNotIn, and the DurationGreaterThan family for duration comparison. The || '' in the example above is an idiom that receives an empty string so that JMESPath evaluation does not fail when the label is absent, and it is needed almost always when dealing with optional fields.

Variables from the request

The variables Kyverno creates in advance come from the AdmissionReview. request.object is the object being created or changed (null on DELETE), request.oldObject is the object before the change (null on CREATE), request.operation is one of CREATE, UPDATE, DELETE, and CONNECT, request.userInfo holds username and groups, and request.namespace is the target namespace. On top of this are added serviceAccountName and serviceAccountNamespace, request.roles and request.clusterRoles, and images, which holds container image information.

There is one trap. If you use in a rule a variable that exists only in the admission request, such as the user name, you must set the policy's background to false. This is because a background scan rescans resources that already exist and so has no request information, and that condition is written out in the kubectl explain output in the CRD documentation.

context — fetching data from elsewhere

context entries are defined inside the rule and are evaluated in the order they are defined. A variable defined earlier can be referenced later, but referencing a later one from an earlier one is an error. There are five kinds.

Kind What it does How to reference
configMap Reads a ConfigMap by name and namespace {{ 이름.data.키 }}
apiCall Calls the Kubernetes API or an external service urlPath + jmesPath
globalReference References a pre-cached GlobalContextEntry name
imageRegistry Fetches the metadata of an OCI image reference + jmesPath
variable Stores a value computed with JMESPath jmesPath + default

In the reference syntax of the configMap row in the table, the two placeholders are the context name and the key.

apiCall uses exactly the same path as kubectl get --raw. That is why the documentation recommends trying it by hand, as follows, before putting it in a policy.

kubectl get --raw /api/v1/namespaces/kyverno/pods | kyverno jp query "items | length(@)"

Moving the same thing into a policy gives this. You can also use variables inside urlPath, the default method is GET, and if you give method: POST and data, you can even call write APIs such as a SubjectAccessReview. To prepare for when the API server returns an error, you can put in a replacement value with default.

context:
- name: podCount
  apiCall:
    urlPath: "/api/v1/namespaces/{{ request.namespace }}/pods"
    jmesPath: "items | length(@)"
    default: 0

For a configMap, if you attach the label cache.kyverno.io/enabled: "true" to the ConfigMap, Kyverno caches it automatically, so it does not call the API server for every policy decision. globalReference goes a step further: if you declare in a GlobalContextEntry resource a kubernetesResource (group, version, resource, namespace) or an apiCall (an external call + refreshInterval), Kyverno maintains the cache with an informer and several policies use the same cache. However, if a GlobalContextEntry is not ready, the policies that reference it also become not ready and are not processed.

Do not forget permissions either. To read some resource with apiCall, the Kyverno controller's ClusterRole must have that permission, and the installation customization documentation guides you to create a ClusterRole with a label such as rbac.kyverno.io/aggregate-to-admission-controller: "true" and aggregate it. Since the wildcard view permission was removed in 1.13, custom resources have to be opened up explicitly.

What it looks like in the field

A team put in the rule "at most two LoadBalancer Services per namespace." match cannot count, so they fetched that namespace's Service list with apiCall, counted with items[?spec.type == 'LoadBalancer'] | length(@), and used GreaterThanOrEquals in preconditions to deny only when it was 2 or more. At first admission latency increased noticeably, and the cause was that one API call went out for every request. As the documentation says, an API call runs on every admission request, so the answer was to move frequently used data into a GlobalContextEntry and cache it.

Another team found that a policy with a user name in its preconditions looked strange in the PolicyReport. A background scan has no request.userInfo, so that policy had to be background: false.

What comes in the next article

The article that follows deals with autogen, where Pod rules are automatically generated into Deployment and CronJob rules, and with cleanup, where policies delete resources. Then, in the quiz, you will check this article's evaluation order, the difference between skip and exclude, and the five kinds of context.