TT Lab
Get started
Learn Learning paths Courses

CKS — Kubernetes Security Specialist

Admission Policy Written in CEL

Continue in TT Lab

Goal

You build yourself a ValidatingAdmissionPolicy that runs with CEL inside the API server with no external webhook server, narrow its scope with a binding, and also write a traditional ValidatingWebhookConfiguration object, checking the difference between the two approaches by hand.

Why it matters

RBAC answers only up to "can you create a Pod." Whether that Pod is privileged or whether its image is from an allowed registry can't be expressed in RBAC. Admission policy fills this gap.

The traditional method is to register an external server with a ValidatingWebhookConfiguration, but it has an availability trap. If the server of a webhook registered with failurePolicy: Fail dies, creation of the target resources is entirely rejected and deployments come to a complete stop. In trying to enforce policy, you bring the cluster down. A ValidatingAdmissionPolicy evaluates CEL expressions inside the API server, so the network hop and the external process disappear, and this failure mode itself shrinks.

The separation of policy and binding is also an intended design. The policy defines only "what counts as a violation," and the binding decides "where and how strongly to apply it." The same policy can be turned on as Warn in staging and as Deny in production.

Steps

  1. Create the namespace cks-admission and attach the label cks-policy=enforce.
  2. Create the ValidatingAdmissionPolicy cks-no-latest-tag. spec.matchConstraints.resourceRules[0] has apiGroups apps, apiVersions v1, operations CREATE and UPDATE, and resources deployments. (The API server rejects a policy that has neither validations nor auditAnnotations. At this step, put in one placeholder validation expression like expression: "true", and replace it with the real expression in step 3.)
  3. Put spec.validations[0] in the same policy. The expression is a CEL that checks that every container image of the Deployment does not end with :latest (it must contain object.spec.template.spec.containers and all), and the message must not be empty.
  4. Set the same policy's spec.failurePolicy to Fail.
  5. Create the ValidatingAdmissionPolicyBinding cks-no-latest-tag-binding. policyName is cks-no-latest-tag, validationActions is Deny, and matchResources.namespaceSelector.matchLabels is cks-policy: enforce.
  6. Create the ValidatingAdmissionPolicy cks-no-privileged. matchConstraints has the core group (apiGroups is an empty string), apiVersions v1, operations CREATE and UPDATE, and resources pods, and validations[0].expression must contain the word privileged and object.spec.containers, and there must be a message too.
  7. Create the ValidatingWebhookConfiguration cks-image-verify. Put in one webhook (name is verify.images.cks.local) with failurePolicy Ignore, sideEffects None, admissionReviewVersions v1, clientConfig.service being the Service image-verifier in the namespace cks-admission, and rules being CREATE on pods in the core group v1.
  8. Create a Deployment checkout in cks-admission. Replicas 2, the selector and Pod label are app=checkout, and also put the label app.kubernetes.io/name=checkout in the Pod template. The container name is app, the image is pinned with @sha256: and does not use :latest, and explicitly set privileged: false and allowPrivilegeEscalation: false in the container securityContext.

Notes

The namespace to apply the policy to

Create the namespace cks-admission and attach the label cks-policy=enforce.

The binding's namespaceSelector looks at this label to pick its targets. Match the label name and value exactly.

The ValidatingAdmissionPolicy skeleton

Create the ValidatingAdmissionPolicy cks-no-latest-tag. spec.matchConstraints.resourceRules[0] has apiGroups apps, apiVersions v1, operations CREATE and UPDATE, and resources deployments. (The API server rejects a policy that has neither validations nor auditAnnotations. At this step, put in one placeholder validation expression like expression: "true", and replace it with the real expression in step 3.)

matchConstraints.resourceRules has the same shape as the rules of an admission webhook. You must fill in all four of apiGroups, apiVersions, operations, and resources. And since the API server rejects creating a policy that has neither validations nor auditAnnotations, at this step put in one placeholder validation expression and replace it with the real expression in the next step.

Write the validation rule in a CEL expression

Put spec.validations[0] in the same policy. The expression is a CEL that checks that every container image of the Deployment does not end with :latest (it must contain object.spec.template.spec.containers and all), and the message must not be empty.

object is the resource being checked. For a Deployment, the containers are in object.spec.template.spec.containers, and you can check all of them with CEL's all() macro.

Decide the failurePolicy

Set the same policy's spec.failurePolicy to Fail.

It is the policy for what to do when expression evaluation fails. For a security policy, it is the side that blocks, not the side that leaves things open.

Turn on the policy with a binding

Create the ValidatingAdmissionPolicyBinding cks-no-latest-tag-binding. policyName is cks-no-latest-tag, validationActions is Deny, and matchResources.namespaceSelector.matchLabels is cks-policy: enforce.

A policy does nothing without a binding. What you put in validationActions decides whether it is a rejection or a warning.

Add a policy forbidding privileged

Create the ValidatingAdmissionPolicy cks-no-privileged. matchConstraints has the core group (apiGroups is an empty string), apiVersions v1, operations CREATE and UPDATE, and resources pods, and validations[0].expression must contain the word privileged and object.spec.containers, and there must be a message too.

Since the target is a Pod, the container path differs from a Deployment. Some containers have no securityContext, so check for existence first in CEL.

Write a ValidatingWebhookConfiguration

Create the ValidatingWebhookConfiguration cks-image-verify. Put in one webhook (name is verify.images.cks.local) with failurePolicy Ignore, sideEffects None, admissionReviewVersions v1, clientConfig.service being the Service image-verifier in the namespace cks-admission, and rules being CREATE on pods in the core group v1.

sideEffects and admissionReviewVersions are required fields, so creation is rejected if they are missing. And since there is no webhook server, choosing the failure policy wrongly halts the cluster.

Putting it together: a deployment that passes the policies

Create a Deployment checkout in cks-admission. Replicas 2, the selector and Pod label are app=checkout, and also put the label app.kubernetes.io/name=checkout in the Pod template. The container name is app, the image is pinned with @sha256: and does not use :latest, and explicitly set privileged: false and allowPrivilegeEscalation: false in the container securityContext.

It must satisfy both policies you created earlier. Use a digest, not a tag, and lower the privileges in the container securityContext.