CKS — Kubernetes Security Specialist
Admission Policy Written in CEL
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
- Create the namespace
cks-admissionand attach the labelcks-policy=enforce. - Create the ValidatingAdmissionPolicy
cks-no-latest-tag.spec.matchConstraints.resourceRules[0]has apiGroupsapps, apiVersionsv1, operationsCREATEandUPDATE, and resourcesdeployments. (The API server rejects a policy that has neithervalidationsnorauditAnnotations. At this step, put in one placeholder validation expression likeexpression: "true", and replace it with the real expression in step 3.) - Put
spec.validations[0]in the same policy. Theexpressionis a CEL that checks that every container image of the Deployment does not end with:latest(it must containobject.spec.template.spec.containersandall), and themessagemust not be empty. - Set the same policy's
spec.failurePolicytoFail. - Create the ValidatingAdmissionPolicyBinding
cks-no-latest-tag-binding.policyNameiscks-no-latest-tag,validationActionsisDeny, andmatchResources.namespaceSelector.matchLabelsiscks-policy: enforce. - Create the ValidatingAdmissionPolicy
cks-no-privileged. matchConstraints has the core group (apiGroups is an empty string), apiVersionsv1, operationsCREATEandUPDATE, and resourcespods, and validations[0].expression must contain the wordprivilegedandobject.spec.containers, and there must be a message too. - Create the ValidatingWebhookConfiguration
cks-image-verify. Put in one webhook (nameisverify.images.cks.local) withfailurePolicyIgnore,sideEffectsNone,admissionReviewVersionsv1,clientConfig.servicebeing the Serviceimage-verifierin the namespacecks-admission, and rules beingCREATEonpodsin the core groupv1. - Create a Deployment
checkoutincks-admission. Replicas 2, the selector and Pod label areapp=checkout, and also put the labelapp.kubernetes.io/name=checkoutin the Pod template. The container name isapp, the image is pinned with@sha256:and does not use:latest, and explicitly setprivileged: falseandallowPrivilegeEscalation: falsein the container securityContext.
Notes
- Check the fields with
kubectl explain validatingadmissionpolicy.spec.matchConstraints.resourceRules. - CEL example:
object.spec.template.spec.containers.all(c, !c.image.endsWith(':latest')) - Example for Pods:
!object.spec.containers.exists(c, has(c.securityContext) && c.securityContext.privileged == true) - Common mistake 1: creating only the policy and not the binding. A policy without a binding checks no requests at all.
- Common mistake 2: if you leave out
sideEffectsoradmissionReviewVersionsfrom the webhook, the API server rejects creating the object itself. - Common mistake 3: using
failurePolicy: Failwhen there is no webhook server makes it impossible to create the target resources at all.
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.