How It Works — Validating Admission Policy

Ivan Sim
9 min readJan 20, 2025

--

The Kubernetes validating admission policy provides a mechanism to allow users to add custom validation policies into the Kubernetes admission control flow. These policies are defined as Kubernetes admission resources, containing validation rules expressed by Common Expression Language (CEL).

By using validating admission policy, users no longer need to hard-code validation policies in validating admission webhook.

This write-up goes over the validating admission policy APIs, code path, metrics and examples on how to enforce immutable and unique resource properties.

The validating admission policy has reached general availability (stable) since Kubernetes 1.30.

What It Is

The admission control flow is made up of a number of admission controllers, which are gatekeeping plugins within the Kubernetes API Server that run to intercept and check authenticated API requests sent to interact with Kubernetes resources. These controllers can mutate and validate Kubernetes resources, and allow and deny these API requests.

kubernetes admission control flow

Prior to validating admission policy, the way to introduce custom validation policies into the admission control flow is to embed them in validating admission webhooks, which run during the validating phase of the admission control flow.

📝 An example of validation policy may be one that includes rules that prohibit all admitted pods from exposing port 80 and binding host path volumes.

These webhooks are usually implemented as 3rd party components that the Kubernetes API server communicates with. They normally come with infrastructure, software and security maintenance overheads.

As an alternative, the in-tree validation admission policy mechanism can be used to safely satisfy many admission validation scenarios.

📝 Admission webhooks are still suitable for use cases that, for example, involve interaction with 3rd party services.

At the heart of this built-in policy framework is the ability to express validation rules in CEL. The CEL is sufficiently lightweight and safe to be run directly in the Kubernetes API Server. It has a straight-forward syntax and grammar, which comes with built-in pre-parsing and type-checking mechanism.

🛠️ The CEL playground is a great tool to help write and verify CEL expressions.

Let’s go over the APIs and resources of the validating admission policy.

API Definition

The validating admission policy is composed of 3 core APIs.

validating admission policy resources

The ValidatingAdmissionPolicy resource defines a collection of CEL-based validation rules. This resource also defines:

  • The kind of resources the policy applies to
  • The kind of resources that are used to parameterize the policy
  • How violation of the policy is reported

To enforce the policy, a corresponding ValidatingAdmissionPolicyBinding resource must be defined. Without it, the policy will have no effects on the rest of the cluster. A single policy can be reused by multiple bindings. Each binding can reference its own set of parameters.

Both the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding are mandatory cluster-scoped resources. The optional parameter resources can be represented by native types such as ConfigMap or custom CRD types. The parameter resources can be scoped to either the cluster level or namespace level. This means parameters can be applied to either the entire cluster or the different namespaces.

Let’s dive into the Kubernetes codes to see how this works 🤓.

How It Works

This code walkthrough aims at providing a sense of how the different Kubernetes components work and where the relevant code are.

Controller Reconciliation

Within the kube-controller-managercode, a controller named validatingadmissionpolicy-status-controller is registered to manage the ValidatingAdmissionPolicy kind:

https://github.com/kubernetes/kubernetes/blob/8f8c94a04d00e59d286fe4387197bc62c6a4f374/cmd/kube-controller-manager/app/controllermanager.go#L580

This controller is started along with other Kubernetes controllers when the kube-controller-manager runs:

https://github.com/kubernetes/kubernetes/blob/2e53799601509de9a031bc3a164f7ad73991e8b8/cmd/kube-controller-manager/app/controllermanager.go#L227-L244

The initFunc routine of the relevant controllerDescriptor initializes the underlying validatingadmissionpolicystatus controller defined in the k8s.io/kubernetes/pkg/controller/validatingadmissionpolicystatuspackage. As its name implied, this controller reconciles the status of the ValidatingAdmissionPolicy kind:

https://github.com/kubernetes/kubernetes/blob/8f8c94a04d00e59d286fe4387197bc62c6a4f374/cmd/kube-controller-manager/app/validatingadmissionpolicystatus.go#L41-L59

The controller’s reconciliation loop invokes the type checker to validate the rules and message expressions defined with the policy’s spec.validations list:

https://github.com/kubernetes/kubernetes/blob/8f8c94a04d00e59d286fe4387197bc62c6a4f374/pkg/controller/validatingadmissionpolicystatus/controller.go#L140-L161
https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go#L110-L140

This reconciliation which involves the expression checks are repeated every time the ValidatingAdmissionPolicy resources are changed.

Resource Admission

During the admission control setup, a validating admission plugin named ValidatingAdmissionPolicy was added to the control flow via the admission.Plugins registration mechanism:

https://github.com/kubernetes/kubernetes/blob/a32f69590a9585adb12952dbc13f7137f37f49bf/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/plugin.go#L77-L81

This plugin implements the admission.ValidationInterface interface and delegates the actual rules validation to the PolicyHook dispatcher:

https://github.com/kubernetes/kubernetes/blob/a32f69590a9585adb12952dbc13f7137f37f49bf/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/plugin.go#L124-L127

The PolicyHook type has an Evaluator that knows how to validate the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding resources:

https://github.com/kubernetes/kubernetes/blob/a32f69590a9585adb12952dbc13f7137f37f49bf/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/plugin.go#L84-L87

The Evaluator provides an implementation of the validating.Validator interface. When the dispatcher received an admission request, it calls the Evaluator to convert the CEL expressions into policy decisions:

https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go#L213-L223

The policy decisions contain actions to be taken following the CEL evaluation, accounting for the policy’s failurePolicy setting.

Expression Cost Analysis

To ensure that the Kubernetes API Server doesn’t get overwhelmed by the CEL expressions evaluation, Kubernetes uses the cel-go cost subsystem to estimate and track the runtime cost of evaluating CEL expressions.

The cost of processing a ValidatingAdmissionPolicy resource is dictated by the complexity of the CEL expressions used to represent the validation rules, compounded by the number of associated ValidatingAdmissionPolicyBinding resources.

Within the Kubernetes code, constant cost budgets, call limit, check frequency and request size are defined to constrain the runtime analysis and evaluation.

https://github.com/kubernetes/kubernetes/blob/1c32094c03f721cd3dce49c9743f1e7f3e564b13/staging/src/k8s.io/apiserver/pkg/apis/cel/config.go#L17-L45

If the cost unit of the rules within a ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding pair exceeds the permitted runtime cost budget, the execution of the expressions will be halted with an error returned.

Following the Dispatcher code above, the validator.Validate() method calculates the cost of evaluating CEL expressions found in the spec.validations[*].expression property, and returns the remaining budget:

https://github.com/kubernetes/kubernetes/blob/e38489303019d442b87611182eb63c94d6e54f03/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator.go#L109-L120

Within the same method, there is also a messageFilter used to evaluate the cost of the CEL expressions found in the spec.validations[*].messageExpression property, and an auditAnnotationFilter which determines the cost of the CEL expressions found in the list of spec.auditAnnotations property.

These filters implement the cel.ConditionEvaluator interface. Within the implementation of the ForInputs() method, the activation.Evaluate() method is invoked to use the cel.Program cost subsystem to calculate the CEL expressions’ actual cost and the policy’s remaining budget:

https://github.com/kubernetes/kubernetes/blob/d8093cc40394b8e25a864576fe6a38306730d3cb/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition.go#L87-L112

If the cost budget is exhausted, the validation failed due to running out of cost budget, no further validation rules will be run error will be returned:

https://github.com/kubernetes/kubernetes/blob/d8093cc40394b8e25a864576fe6a38306730d3cb/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/activation.go#L141-L180

Hopefully, the code is not too difficult to follow.

The next section explores two examples on how to use the validating admission policy to enforce immutable and unique resource specification.

Examples

Example 1 — Immutable Resource Specification

Imagine a CRD named vnodes.virt.dev which is used to represent virtual nodes running in Kubernetes.

An example VNode instance looks like this:

apiVersion: virt.dev/v1alpha1
kind: VNode
metadata:
name: vnode
labels:
virt.dev/immutability: enforced
spec:
cpu:
cores: 1
sockets: 1
threads: 1
memory:
guest: 3996Mi
devices:
disks:
- disk:
bus: virtio
name: disk-0
interfaces:
- macAddress: da:5a:20:f5:e4:ce
masquerade: {}
model: virtio
name: default

It is possible to define a simple validating admission policy with rules to ensure that once a VNode resource is created, its specification remains immutable.

💡 Using the validating admission policy like this is useful when it isn’t easy to directly modify the CRD schema with transition rules.

This is what the ValidatingAdmissionPolicy looks like:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: vnodes-immutability
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["virt.dev"]
apiVersions: ["v1alpha1"]
operations: ["UPDATE"]
resources: ["vnodes"]
validations:
- expression: "oldObject.spec == object.spec"
message: "VNode spec is immutable"
reason: Invalid

Here’s quick explanation on some of the properties:

  • metadata.name — This is the name of the policy. All associated ValidatingAdmissionPolicyBinding resources must reference this name.
  • spec.failurePolicy — This value defines the handling of policy failures which include cases like invalid CEL expressions, misconfigured bindings, non-existent parameter kinds etc. However, it doesn’t specify what to do with validations that are evaluated to false. That is done by the ValidatingAdmissionPolicyBinding resource’s spec.validationActions property.
  • spec.matchConstraints — This property defines the kinds of resources that the policy validates.
  • spec.validations — This is the list of validation rules and their associated messages, represented by CEL expressions.

The policy is not enforced until an associated ValidatingAdmissionPolicyBinding resource is deployed.

The following ValidatingAdmissionPolicyBinding resource references the vnodes-immutability policy. It refines the the resource matching criteria to just VNodes resources with the virt.dev/immutability: enforced label:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: vnodes-immutability
spec:
policyName: vnodes-immutability
validationActions: [Deny]
matchResources:
objectSelector:
matchLabels:
virt.dev/immutability: enforced

With this policy binding deployed, any attempt to update the previously created VNode resource specification will fail.

For example, attempting to use kubectl edit vnode vnode to change the value of the spec.cpu.cores property of the previous VNode resource will result in the following error message:

# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
# vnodes.virt.dev "vnode" was not valid:
# * : ValidatingAdmissionPolicy 'vnodes-immutability' with binding 'vnodes-immutability' denied request: VNode spec is immutable
#

👀 The VNode spec is immutable error message comes from the ValidatingAdmissionPolicy resource.

Example 2 — Unique Specification Property

Let’s extend the VNode specification with an optional .spec.staticIP:

apiVersion: virt.dev/v1alpha1
kind: VNode
metadata:
name: vnode2
labels:
virt.dev/immutability: enforced
spec:
staticIP: 192.168.255.100 # new property
cpu:
cores: 1
sockets: 1
threads: 1
memory:
guest: 3996Mi
devices:
disks:
- disk:
bus: virtio
name: disk-0
interfaces:
- macAddress: da:5a:20:f5:e4:ce
masquerade: {}
model: virtio
name: default

We can use the following ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding resources to ensure every VNode resource gets an unique static IP:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: vnodes-unique-static-ip
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["virt.dev"]
apiVersions: ["v1alpha1"]
operations: ["CREATE","UPDATE"]
resources: ["vnodes"]
validations:
- expression: "params.allocations.all(e, e.ipAddress != object.spec.staticIP)"
messageExpression: "'invalid static IP. conflicting allocation: '
+ params.allocations.filter(e, e.ipAddress == object.spec.staticIP)[0].hwAddress
+ '/'
+ params.allocations.filter(e, e.ipAddress == object.spec.staticIP)[0].ipAddress"
reason: Invalid
paramKind:
apiVersion: virt.dev/v1alpha1
kind: StaticIPAllocation
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: vnodes-unique-static-ip
spec:
policyName: vnodes-unique-static-ip
validationActions: [Deny]
paramRef:
name: allocated
namespace: default
parameterNotFoundAction: Deny

The policy references the StaticIPAllocation kind as its parameter resource. This CRD is used to track the list of allocated (hence, unavailable) static IPs.

The custom controller that manages StaticIPAllocation resources is not included in this post.

The policy binding definition narrows down the reference relationship to a specific instance of the StaticIPAllocation kind named allocated in the default namespace.

An example StaticIPAllocation resource looks like:

apiVersion: virt.dev/v1alpha1
kind: StaticIPAllocation
metadata:
name: allocated
allocations:
- ipAddress: 192.168.255.100
hwAddress: da:5a:20:f5:e4:ce
- ipAddress: 192.168.255.108
hwAddress: 00:1A:2B:3C:4D:5E
- ipAddress: 192.168.255.167
hwAddress: 2C:54:91:88:C9:E3

Attempting to deploy the vnode2 virtual node which requests for the static IP 192.168.255.100 will fail with the following error:

The vnodes "vnode2" is invalid: : ValidatingAdmissionPolicy 'vnodes-unique-static-ip' with binding 'vnodes-unique-static-ip' denied request: 
conflicting static IP allocation exists: da:5a:20:f5:e4:ce/192.168.255.100

💡 Since the StaticIPAllocation kind is namespace-scoped, multiple parameter sets can be defined to enforce different constrains in different namespaces.

Prometheus Metrics

With Prometheus installed, the apiserver_validating_admission_policy_check_duration_seconds can be used to determine the policy violation count and latency:

Prometheus chart showing the number of policy violations

The Prometheus histogram_quantile function can be used to calculate the φ-quantile of check duration for specific policy binding pairs, over a certain time interval.

The following sample graph shows the 99-percentile check duration (in seconds) of the vnodes-immutability policy:

Prometheus chart showing 99-percentile of policy check duration (seconds) of the vnodes-immutability policy

The next graph shows the 99-percentile check duration (in seconds) of the vnodes-unique-static-ip policy:

Prometheus chart showing 99-percentile of policy check duration (seconds) of the vnodes-static-unique-ip policy

These metrics can be used to configure alerts when policy failures or evaluation latency exceed certain SLO thresholds.

Conclusion

The Kubernetes validating admission policy allows users to add validation policy to the admission control workflow. The validation rules, expressed as CEL expressions, are checked, evaluated and run by the Kubernetes API Server. This provides an alternative to implementing validating admission webhooks with hard-coded validation rules.

The code walkthrough in this post shows how the validating admission policy feature is implemented as a Kubernetes controller and an admission plugin.

Two examples are included to demonstrate how to utilize CEL expressions within the ValidatingAdmissionPolicy resource to enforce immutable and unique resource properties.

Finally, Kubernetes exposes key metrics to calculate failed policy counts and policy check duration latency.

--

--

Ivan Sim
Ivan Sim

Responses (1)