Preconditions

Fine-grained control of policy rule execution based on variables and expressions.

Preconditions allow for more fine-grained selection of resources than the options allowed by match and exclude statements. Preconditions consist of one or more expressions which are evaluated after a resource has been successfully matched (and not excluded) by a rule. They are powerful in that they allow you access to variables, JMESPath filters, operators, and other constructs which can be used to precisely select when a rule should be evaluated. When preconditions are evaluated to an overall TRUE result, processing of the rule body begins.

For example, if you wished to apply policy to all Kubernetes Services which were of type NodePort, since neither the match/exclude blocks provide access to fields within a resource’s spec, a precondition could be used. In the below rule, while all Services are initially selected by Kyverno, only the ones which have the field spec.type set to NodePort will go on to be processed to ensure the field spec.externalTrafficPolicy equals a value of Local.

 1rules:
 2- name: validate-nodeport-trafficpolicy
 3  match:
 4    any:
 5    - resources:
 6        kinds:
 7        - Service
 8  preconditions:
 9    all:
10    - key: "{{ request.object.spec.type }}"
11      operator: Equals
12      value: NodePort
13  validate:
14    message: "All NodePort Services must use an externalTrafficPolicy of Local."
15    pattern:
16      spec:
17        externalTrafficPolicy: Local

While, in the above snippet, a precondition is used, it would have been possible to also express this desire using multiple types of anchors instead. It is more common for preconditions to exist when needing to perform more advanced comparisons between context data (e.g., results from a stored ConfigMap, Kubernetes API call, service call, etc.) and admission data. In the below snippet, a precondition is used to measure the length of an array of volumes coming from a Pod which are of type hostPath. Preconditions can use context variables, the JMESPath system, and perform comparisons between the two and more.

 1rules:
 2- name: check-hostpaths
 3  match:
 4    any:
 5    - resources:
 6        kinds:
 7        - Pod
 8  context:
 9  - name: hostpathvolnames
10    variable:
11      jmesPath: request.object.spec.volumes[?hostPath].name
12      default: []
13  preconditions:
14    all:
15    - key: "{{ length(hostpathvolnames) }}"
16      operator: GreaterThan
17      value: 0

Preconditions are similar in nature to deny rules in that they are built of the same type of expressions and have the same fields. Also like deny rules, preconditions use short circuiting to stop or continue processing depending on whether they occur in an any or all block.

Because preconditions very commonly use variables in JMESPath format (e.g., {{ request.object.spec.type }}), there are some special considerations when it comes to their formatting. See the JMESPath formatting page for further details.

When preconditions are used in the rule types which support reporting, a result will be scored as a skip if a resource is matched by a rule but discarded by the combined preconditions. Note that this result differs from if it applies to an exclude block where the resource is immediately ignored.

Preconditions are also used in mutate rules inside a foreach loop for more granular selection of array entries to be mutated. See the documentation here for more details.

Any and All Statements

Preconditions are evaluated by nesting the expressions under any and/or all statements. This gives you further power in building more precise logic for how the rule is triggered. Either or both may be used simultaneously in the same rule. For each any/all statement, each block must overall evaluate to TRUE for the precondition to be processed. If any of the any / all statement blocks does not evaluate to TRUE, preconditions will not be satisfied and thus the rule will not be applicable.

For example, consider a Deployment manifest which features many different labels as follows.

 1apiVersion: apps/v1
 2kind: Deployment
 3metadata:
 4  name: busybox
 5  labels:
 6    app: busybox
 7    color: red
 8    animal: cow
 9    food: pizza
10    car: jeep
11    env: qa
12spec:
13  replicas: 1
14  selector:
15    matchLabels:
16      app: busybox
17  template:
18    metadata:
19      labels:
20        app: busybox
21    spec:
22      containers:
23      - image: busybox:1.28
24        name: busybox
25        command: ["sleep", "9999"]

By using any and all blocks in the preconditions statement, it is possible to gain more granular control over when rules are evaluated. In the below sample policy, using an any block will allow the preconditions to work as a logical OR operation. This policy will only perform the validation if labels color=blue OR app=busybox are found. Because the Deployment manifest above specified color=red, using the any statement still allows the validation to occur.

 1apiVersion: kyverno.io/v1
 2kind: ClusterPolicy
 3metadata:
 4  name: any-all-preconditions
 5spec:
 6  validationFailureAction: Enforce
 7  background: false
 8  rules:
 9  - name: any-all-rule
10    match:
11      any:
12      - resources:
13          kinds:
14          - Deployment
15    preconditions:
16      any:
17      - key: "{{ request.object.metadata.labels.color || '' }}"
18        operator: Equals
19        value: blue
20      - key: "{{ request.object.metadata.labels.app || '' }}"
21        operator: Equals
22        value: busybox
23    validate:
24      message: "Busybox must be used based on this label combination."
25      pattern:
26        spec:
27          template:
28            spec:
29              containers:
30              - name: "*busybox*"

Adding an all block means that all of the statements within that block must evaluate to TRUE for the whole block to be considered TRUE. In this policy, in addition to the previous any conditions, it checks that all of animal=cow and env=qa but changes the validation to look for a container with name having the string foxes in it. Because the any block and all block evaluate to TRUE, the validation is performed, however the Deployment will fail to create because the name is still busybox. If one of the statements in the all block is changed so the value of the checked label is not among those in the Deployment, the rule will not be processed and the Deployment will be created.

 1apiVersion: kyverno.io/v1
 2kind: ClusterPolicy
 3metadata:
 4  name: any-all-preconditions
 5spec:
 6  validationFailureAction: Enforce
 7  background: false
 8  rules:
 9  - name: any-all-rule
10    match:
11      any:
12      - resources:
13          kinds:
14          - Deployment
15    preconditions:
16      any:
17      - key: "{{ request.object.metadata.labels.color || '' }}"
18        operator: Equals
19        value: blue
20      - key: "{{ request.object.metadata.labels.app || '' }}"
21        operator: Equals
22        value: busybox
23      all:
24      - key: "{{ request.object.metadata.labels.animal || '' }}"
25        operator: Equals
26        value: cow
27      - key: "{{ request.object.metadata.labels.env || '' }}"
28        operator: Equals
29        value: qa
30    validate:
31      message: "Foxes must be used based on this label combination."
32      pattern:
33        spec:
34          template:
35            spec:
36              containers:
37              - name: "*foxes*"

Operators

The following operators are currently supported in conditional expressions wherever expressions are used:

  • Equals
  • NotEquals
  • AnyIn
  • AllIn
  • AnyNotIn
  • AllNotIn
  • GreaterThan
  • GreaterThanOrEquals
  • LessThan
  • LessThanOrEquals
  • DurationGreaterThan
  • DurationGreaterThanOrEquals
  • DurationLessThan
  • DurationLessThanOrEquals

Operators Equals and NotEquals are only applicable when comparing a single key to a single value and not an array. Set operators should be used instead in those cases.

The set operators, AnyIn, AllIn, AnyNotIn and AllNotIn, are the most commonly-used and most flexible operators which support one-to-one, one-to-many, many-to-one, and many-to-many comparisons. They support string, integer, and boolean types.

  • AnyIn: checks that ANY of the keys are found in the values.
  • AllIn: checks that ALL of the keys are found in the values.
  • AnyNotIn: checks that ANY of the keys are NOT found in the values.
  • AllNotIn: checks that ALL of the keys are NOT found in the values.

The duration operators can be used for things such as validating an annotation that is a duration unit. Duration operators expect numeric key or value as seconds or as a string that is a valid Go time duration, eg: “1h”. The string units supported are s (second), m (minute) and h (hour). Full details on supported duration strings are covered by time.ParseDuration.

The GreaterThan, GreaterThanOrEquals, LessThan and LessThanOrEquals operators can also be used with Kubernetes resource quantities. Any value handled by resource.ParseQuantity can be used, this includes comparing values that have different scales. Note that these operators can only operate on a single value currently and not an array of values, even if the array contains a single string.

Example:

 1apiVersion: kyverno.io/v1
 2kind: ClusterPolicy
 3metadata:
 4  name: resource-quantities
 5spec:
 6  validationFailureAction: Enforce
 7  background: false
 8  rules:
 9  - name: memory-limit
10    match:
11      any:
12      - resources:
13          kinds:
14          - Pod
15    preconditions:
16      any:
17      - key: "{{request.object.spec.containers[0].resources.requests.memory}}"
18        operator: LessThan
19        value: 1Gi

Wildcard Matches

String values support the use of wildcards to allow for partial matches. The following example matches on Ingress resources where the first rule does not have a host which ends in .mycompany.com.

 1- name: mutate-rules-host
 2  match:
 3    resources:
 4      kinds:
 5      - Ingress
 6  preconditions:
 7    all:
 8    - key: "{{request.object.spec.rules[0].host}}"
 9      operator: NotEquals
10      value: "*.mycompany.com"

Matching requests without a service account

Preconditions have access to predefined variables from Kyverno further extending their power.

In this example, the rule is only applied to requests from ServiceAccounts (i.e. when the {{serviceAccountName}} variable is not empty).

 1  - name: generate-owner-role
 2    match:
 3      any:
 4      - resources:
 5          kinds:
 6          - Namespace
 7    preconditions:
 8      any:
 9      - key: "{{serviceAccountName}}"
10        operator: NotEquals
11        value: ""

Matching requests from specific service accounts

Preconditions support providing key and value fields as lists as well as simple strings.

In this example, the rule is only applied to requests from a ServiceAccount with name build-default and build-base.

 1  - name: generate-default-build-role
 2    match:
 3      any:
 4      - resources:
 5          kinds:
 6          - Namespace
 7    preconditions:
 8      any:
 9      - key: "{{serviceAccountName}}"
10        operator: AnyIn
11        value:
12        - build-default
13        - build-base

Adding custom messages

Although preconditions do not produce a blocking effect similar to deny rules, they are capable of showing a custom message when an expressions fails. The message will be shown in the Kyverno log. The rule snippet below will print the message specified in the logs for the expression which evaluates to FALSE keeping in mind short circuiting.

 1- name: message-rule
 2  match:
 3    any:
 4    - resources:
 5        kinds:
 6          - ConfigMap
 7  preconditions:
 8    all:
 9    - key: "{{ request.object.data.food }}"
10      operator: Equals
11      value: cheese
12      message: My favorite food is cheese.
13    - key: "{{ request.object.data.day }}"
14      operator: Equals
15      value: monday
16      message: You have a case of the Mondays.

Last modified April 08, 2024 at 8:29 AM PST: Refactor links (#1205) (5060f3d)