There is a single moment in a resource's lifecycle where you can say "no" before anything happens: admission. After the API server authenticates and authorizes a request, and before it persists the object to etcd, the request passes through the admission chain. This is the chokepoint where policy lives — where you can reject a privileged pod, require a signed image, mandate resource limits, or block a LoadBalancer service that would expose an internal app to the internet. Everything downstream (Pod Security Admission, network policy, runtime detection) assumes the object already exists; admission is your chance to prevent it from existing at all. This guide covers the mechanics and the current tooling, including the CEL-based policies that changed the landscape in recent releases.
Validating vs mutating
Admission controllers come in two flavors, and the distinction matters:
- Validating controllers inspect a request and either allow or reject it. They cannot change the object. Use them to enforce rules: "reject any pod without
runAsNonRoot." - Mutating controllers can modify the request before it is stored — injecting a sidecar, adding a default
securityContext, setting a label. Mutating webhooks run before validating ones, which is why a mutating webhook can add a default that a validating webhook then checks.
For security enforcement, favor validating policies. Mutation is powerful but it silently changes what the user submitted, which can mask problems and complicate audits.
The built-in webhook mechanism
Both flavors are wired in through ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects that point the API server at an HTTPS endpoint you run. The most important field from a security standpoint is failurePolicy. If your webhook is down and failurePolicy: Ignore is set, every request sails through unchecked — the policy silently stops enforcing exactly when the service is unhealthy. For security controls, use Fail:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: enforce-security
webhooks:
- name: pods.policy.example.com
failurePolicy: Fail # closed, not open
timeoutSeconds: 5
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
admissionReviewVersions: ["v1"]
sideEffects: None
clientConfig:
service:
name: policy-webhook
namespace: policy-system
path: /validate
Running your own webhook means running a highly available service, managing its TLS certificate, and keeping latency low, since it sits in the critical path of every matching request.
Policy engines: Kyverno and OPA Gatekeeper
Most teams do not hand-write webhooks; they use a policy engine that provides the webhook and a declarative way to author rules.
Kyverno expresses policy as Kubernetes-native YAML, which is approachable for teams that already live in manifests:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-privileged
spec:
validationFailureAction: Enforce
rules:
- name: no-privileged-containers
match:
any:
- resources:
kinds: [Pod]
validate:
message: "Privileged containers are not allowed."
pattern:
spec:
=(securityContext):
=(privileged): "false"
containers:
- =(securityContext):
=(privileged): "false"
OPA Gatekeeper uses Rego, a purpose-built policy language that is more expressive for complex logic at the cost of a steeper learning curve. It shines when policies need rich cross-resource reasoning.
The native option: ValidatingAdmissionPolicy (CEL)
The significant recent change is that Kubernetes now ships a built-in, in-process policy mechanism that needs no external webhook. ValidatingAdmissionPolicy, which reached GA in v1.30, uses the Common Expression Language (CEL) to evaluate rules inside the API server itself — eliminating the webhook's availability, latency, and TLS-management burden:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-non-root
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "object.spec.securityContext.runAsNonRoot == true"
message: "Pods must set securityContext.runAsNonRoot: true"
Because it runs in-process, there is no webhook to fail open and nothing extra to keep highly available. The trade-off is that CEL cannot make external calls or maintain state, so image-signature verification and cross-resource lookups still belong to Kyverno or Gatekeeper. A mutating counterpart, MutatingAdmissionPolicy, has been maturing through recent releases for the cases where you need CEL-based defaulting. Use ValidatingAdmissionPolicy for the many simple field-level rules and reserve full policy engines for the rules that genuinely need them.
Choosing between them
| Need | Best fit |
|---|---|
Simple field checks (non-root, no :latest) | ValidatingAdmissionPolicy (CEL) |
| YAML-native policy, image verification | Kyverno |
| Complex, cross-resource logic | OPA Gatekeeper (Rego) |
| Sidecar injection, defaulting | Mutating webhook / MutatingAdmissionPolicy |
| Fail-closed enforcement | Any, with failurePolicy: Fail |
Rollout guidance
- Start in audit/warn. Both Kyverno (
Audit) and ValidatingAdmissionPolicy (warn/audit) let you observe violations before blocking, so you find the legitimate exceptions first. - Fail closed for security policies. A control that fails open is not a control.
- Scope match rules tightly. A webhook matching
["*"]on every resource adds latency to the whole cluster and expands blast radius if it misbehaves. - Version and test policies as code. Policies are code; put them in Git, review them, and test them against known-bad manifests in CI.
How Safeguard helps
Admission policies enforce at the cluster edge, but the same rules should be checked far earlier, when a manifest is still a pull request. Safeguard's IaC scanning evaluates Kubernetes manifests and Helm charts against the same class of rules your admission controllers enforce — non-root, dropped capabilities, no privileged containers, signed images — so violations are caught in review instead of bouncing off admission in staging. It also inspects your ValidatingWebhookConfiguration and policy objects for the dangerous patterns, like failurePolicy: Ignore on a security webhook, that quietly disable enforcement. Container security scanning supplies the image posture that image-verification policies depend on, and Griffin AI prioritizes which policy gaps matter most given real exploitability. Compare approaches across the ecosystem on the comparison hub.
Admission is your last chance to say no — make the rules count on both sides of the gate. Create a free Safeguard account or read the documentation to shift these checks left.