✉️ In Today’s Edition

In today’s edition, we’ll look at an important Kubernetes concept and workflow every DevOps engineer should understand: admission policies.

Here’s what we’ll cover:

  • How dynamic admission control works in Kubernetes

  • What native admission policies are

  • ValidatingAdmissionPolicy vs MutatingAdmissionPolicy

  • Hands-on examples for validation and mutation

  • Native admission policies vs Kyverno and Gatekeeper

  • When you still need admission webhooks

And more…

40% OFF Linux Foundation Certifications (Exclusive Offer)

We’ve partnered with the Linux Foundation to offer our newsletter subscribers an exclusive 40% discount on Linux Foundation certifications. If you are planning to take a Kubernetes, Linux, Cloud, or DevOps certification, this is a good time to save.

Use code SUM26CT at kube.promo/devops to get a flat 40% off.

For Kubernetes certification bundles, you can save more than 45% with the same coupon.

If you know someone preparing for a certification, please share this offer with them too.

Policy as code is a key best practice in production Kubernetes clusters. For example, you can enforce policies such as requiring every pod to have resource limits or ensuring images are pulled only from a specific private registry.

To enforce such policies in a Kubernetes cluster, teams usually use tools such as Kyverno or OPA Gatekeeper.

These tools typically run as custom dynamic admission controllers and integrate with the Kubernetes API server using custom admission webhooks.

Let's understand how this works.

Dynamic Admission Controllers

Note: If you are already aware of how admission controllers work, skip this section and move on to the next one.

An admission controller is a small piece of code that validates or modifies Kubernetes objects before they are persisted.

The following diagram illustrates how a dynamic admission controller, such as Kyverno, enforces policies via Kubernetes admission webhooks.

Here is how it works.

When a user sends a request, such as creating a Pod using kubectl, the request first reaches the Kubernetes API server.

The API server first authenticates the request to verify the identity of the user or service account.

Next, the request goes through authorization to check whether that user has permission to perform the requested action. For example, whether the user is allowed to create Pods in a particular namespace.

If the request is authorized, it moves to the admission control stage. Admission control mainly applies to requests that create, modify, delete, or connect (proxy requests) resources. Read-only requests such as GET, LIST, and WATCH bypass admission control.

During the admission process, the API server checks whether the request matches any configured admission webhooks. For example, a Pod creation event. If it matches, the API server sends the request to the webhook as an AdmissionReview object.

There are two types of admission webhooks.

  • Validating webhooks can allow or reject the request. For example, if a Deployment doesn’t have a specific label, the creation request gets rejected.

  • Mutating webhooks can modify the resource before it is created or updated. A classic example of this is Istio, which uses a mutating webhook to automatically inject the sidecar container into the pod spec.

The dynamic admission controller then evaluates the request against the defined policies. For example, a Kyverno policy that requires the image URL to come from a specific private registry.

Once the policy check is complete, the admission controller sends the response back to the API server. If the request is allowed, Kubernetes proceeds with the requested operation.

Important Note: This means tools such as Kyverno and OPA Gatekeeper require a custom external admission controller and webhook setup to enforce these policies.

However, you can implement many of these policy use cases using Kubernetes native functionality without running an external admission webhook.

This is where native Kubernetes Admission Policies come into the picture.

First, let's understand what an admission policy is.

What is an AdmissionPolicy?

Unlike custom admission controllers, Kubernetes Admission Policies provide a native way to validate or modify API requests during admission, without running a separate admission webhook server.

This means you don’t need to deploy and manage custom admission controllers and webhooks to validate or modify API objects based on your requirements.

There are two Native Admission Policy types:

  • ValidatingAdmissionPolicy - This checks the incoming API request and decides whether it should be allowed.

  • MutatingAdmissionPolicy - This changes the incoming resource before it is created.

Note: ValidatingAdmissionPolicy became GA in version 1.30, and MutatingAdmissionPolicy became stable in 1.36.

Common Expression Language (CEL).

The admission policies are written in Common Expression Language (CEL). It is a lightweight expression language used by Kubernetes. It's the same language used for CRD validation rules.

A CEL expression evaluates true or false. If it returns false, the request is denied.

For example, object.spec.replicas <= 5.

This expression will deny the request if the replica count exceeds 5. We will look at the same example in the hands-on part.

Let's look at ValidatingAdmissionPolicy first.

What is ValidatingAdmissionPolicy?

ValidatingAdmissionPolicy is the native alternative to validating admission webhooks built directly into the API server.

Instead of running a separate HTTP server that the API server calls on every request, the validation logic lives directly inside the API server as a native object.

If a request violates a rule, the API server will reject it as shown in the image below.

Next, we will look at how to create a native ValidatingAdmissionPolicy.

Creating ValidatingAdmissionPolicy

To understand the policy, we will create a simple policy that blocks Deployments with more than 5 replicas.

Here is the ValidatingAdmissionPolicy manifest with a CEL validation expression.

kubectl apply -f - <<EOF
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "limit-replicas-policy"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups:   ["apps"]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["deployments"]
  validations:
  - expression: "object.spec.replicas <= 5"
    message: "Deployments in this namespace cannot have more than 5 replicas."
EOF

In the above manifest, one thing people often misunderstand is failurePolicy: Fail. It determines what happens if Kubernetes cannot evaluate the policy correctly due to issues such as a CEL runtime error or policy misconfiguration.

Fail means reject the request instead of letting it through, and the other option is Ignore, which accepts the request.

Now we have the rule, but it doesn't do anything until we create a binding. You can create a binding for this policy using the ValidatingAdmissionPolicyBinding object.

Let's create a binding for it.

The binding manifest below applies the policy in the default namespace.

kubectl apply -f - <<EOF
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "limit-replicas-policy-binding"
spec:
  policyName: "limit-replicas-policy"
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: default
EOF

Here, validationActions is the field that specifies the action the admission controller should take if an API request violates the policy. We have used Deny, which rejects requests that fail the validation.

There are two more options Warn and Audit.

Tip: If you are introducing a new policy into the cluster, start with the Warn action. This allows you to identify requests that violate the policy without blocking them. Once you have verified that the policy works as expected, you can change the action to Deny to enforce it.

Once both objects are created, check the policy by creating a deployment with 5 replicas in the default namespace using the following command. It should be created without any issues.

kubectl create deployment nginx-1 --image=nginx --replicas=5

Now, create a deployment with 6 replicas in the default namespace using the following command.

kubectl create deployment nginx-2 --image=nginx --replicas=6

This will be blocked because the limit is 5 replicas for a deployment. You will get the following error message.

error: failed to create deployment: deployments.apps "nginx-2" is forbidden: ValidatingAdmissionPolicy 'limit-replicas-policy' with binding 'limit-replicas-policy-binding' denied request: Deployments in this namespace cannot have more than 5 replicas.

This means the policy worked.

Why Do Admission Policies Use Separate Policies and Bindings?

The Policy defines the rule logic, and it's reusable. The Binding specifies where the rule applies, which resources it applies to, and what action to take upon violation.

If they were one object, every time you wanted to apply the same rule to a different namespace or with a different action, you would have to duplicate the entire policy.

It's the same reason Kubernetes separates ClusterRole from ClusterRoleBinding. One has the role, and the other binds to different resources.

So far, we have only validated the incoming requests.

But what if you want Kubernetes to automatically modify the resource?

That's where MutatingAdmissionPolicy comes in.

What is MutatingAdmissionPolicy?

MutatingAdmissionPolicy is the native alternative to a mutating admission webhook.

Instead of only validating the incoming object, it can modify the object during admission. For example, it can add default labels or configuration.

Just like validation policies, the mutation logic runs directly inside the API server and is written using CEL. It supports mutations using either an ApplyConfiguration or JSON Patch.

The following image illustrates how a Kubernetes MutatingAdmissionPolicy modifies a resource before it is created.

When an admin creates the namespace, the API Server adds the label to it.

Let’s put that use case into practice.

Create a MutatingAdmissionPolicy

The following manifest creates a mutating policy that automatically adds a label to every newly created namespace.

kubectl apply -f - <<EOF
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata:
  name: add-namespace-label
spec:
  failurePolicy: Fail
  reinvocationPolicy: IfNeeded
  matchConstraints:
    resourceRules:
    - apiGroups: [""]
      apiVersions: ["v1"]
      operations: ["CREATE"]
      resources: ["namespaces"]
  mutations:
  - patchType: ApplyConfiguration
    applyConfiguration:
      expression: >
        Object{
          metadata: Object.metadata{
            labels: {"managed-by": "devops-team"}
          }
        }
EOF

Here you can see that under the mutations block, we tell Kubernetes to modify the incoming object using an ApplyConfiguration.

Now, let’s create a binding for the policy, as we did before.

kubectl apply -f - <<EOF
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata:
  name: add-namespace-label-binding
spec: 
  policyName: add-namespace-label
EOF

Unlike a validating binding, there is no separate action field for options such as Deny, Warn, or Audit

Here, when a request matches the policy through a MutatingAdmissionPolicyBinding, the only action is to mutate the object.

Now let's test it by creating a new namespace.

kubectl create namespace demo

Then check the labels of the newly created namespace.

kubectl get namespace demo --show-labels

You should see the label added automatically.

NAME STATUS AGE   LABELS
demo Active 4s    kubernetes.io/metadata.name=demo,managed-by=devops-team

The namespace request came in without the label. The API server evaluated our policy, added the label, and then stored the modified Namespace.

Do Native Policies Replace Kyverno or Gatekeeper?

Native admission policies can now handle both validation and mutation, which covers many common policy requirements. However, they're not a complete replacement for tools like Kyverno.

The following are the current limitations of native admission policies.

  • Native policies don't cover every workflow available in dedicated policy engines, such as generating additional Kubernetes resources.

  • Tools like Kyverno can automatically create a NetworkPolicy or a RoleBinding whenever a new namespace is created. Native policies cannot do this.

  • CEL runs in the API server. It cannot call an external API, query a database, or check an OCI registry. If your validation logic needs to reach outside the cluster, you need a webhook.

  • CEL is good for simple policies, but for complex policies, dedicated policy engines like Kyverno are recommended.

So, you can use native admission policies for straightforward admission rules and use tools such as Kyverno or Gatekeeper when your policy requirements go beyond what native admission policies can handle.

Wrapping Up

In summary, with ValidatingAdmissionPolicy, the API server checks whether the request violates the policy and either allows or rejects it.

And, with MutatingAdmissionPolicy, the API server modifies the specified object type.

Native policies can only validate or modify an object; they cannot create resources, verify image signatures, or access resources outside the cluster.

Tools like Kyverno and OPA/Gatekeeper cover those gaps. So, choose according to your needs.

Reply

Avatar

or to participate