Skip to main content
RunBook Academy

KubernetesIII · Kubernetes APIKubernetes API

Admission control — mutating, validating, and policy enforcement

Intermediate⏱ ~18 minkubectl

What you'll learn

  • Distinguish mutating admission from validating admission and identify built-in admission controllers
  • Configure Pod Security Standards, LimitRanger, ResourceQuota, and other built-in admission for a namespace
  • Design webhook admission with HA, failurePolicy, and observability
  • Use ValidatingAdmissionPolicy (CEL-based) to enforce declarative policies

Prerequisites

Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16

Not yet marked complete on this device.

Admission control is the layer of the API server pipeline that enforces policy on objects, not on identities. Where authorisation answers “is this identity allowed to do this?”, admission answers “is this object safe to admit?”. This lesson covers built-in admission controllers, custom webhooks, and the new ValidatingAdmissionPolicy system.

Where admission fits

flowchart LR
    R[Request] --> AUTHN[Authentication]
    AUTHN --> AUTHZ[Authorisation]
    AUTHZ --> MA[Mutating Admission]
    MA --> VA[Validating Admission]
    VA --> SV[Schema validation]
    SV --> P[Persistence]

Admission runs after authentication and authorisation but before persistence. This order means admission only processes requests that are already authenticated and authorised — admission’s job is to inspect and possibly modify the object, then either accept or reject.

Built-in admission controllers

The API server runs a set of built-in admission controllers as part of the chain. The defaults vary by version; production clusters explicitly enable the ones they want.

PodSecurity (Pod Security Standards)

Enforces privileged, baseline, or restricted Pod Security Standards at the namespace level. Configured via namespace labels:

kubectl label namespace team-a-prod \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest \
  pod-security.kubernetes.io/warn=baseline \
  pod-security.kubernetes.io/warn-version=latest \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/audit-version=latest

The three modes per namespace:

  • enforce — reject Pods that violate the level
  • warn — allow but emit a warning event
  • audit — allow but emit an audit log entry

Production: restricted is the strictest; baseline is the common compromise. Run restricted for new namespaces; allow older namespaces to migrate.

LimitRanger

Applies LimitRange defaults to Pods in the namespace. Configured by:

apiVersion: v1
kind: LimitRange
metadata:
  name: default
  namespace: team-a-prod
spec:
  limits:
  - type: Container
    default:
      cpu: 500m
      memory: 512Mi
    defaultRequest:
      cpu: 100m
      memory: 128Mi
    max:
      cpu: 2
      memory: 4Gi
    min:
      cpu: 50m
      memory: 32Mi

LimitRanger also validates Pods against the LimitRange (rejecting if max is exceeded).

ResourceQuota

Enforces ResourceQuota for the namespace:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-a-prod
  namespace: team-a-prod
spec:
  hard:
    pods: "200"
    requests.cpu: "64"
    requests.memory: 128Gi
    limits.cpu: "128"
    limits.memory: 256Gi
    persistentvolumeclaims: "50"
    requests.storage: 1Ti
    services: "100"
    secrets: "100"
    configmaps: "100"
    count/deployments.apps: "50"

ResourceQuota rejects Pods that would push the namespace over the quota. The count/... syntax counts objects of a specific kind.

ServiceAccount admission

Injects the default ServiceAccount token if spec. serviceAccountName is not set; configures automountServiceAccountToken defaults.

ImagePolicyWebhook (deprecated)

Older webhook for image allowlists. Superseded by custom webhooks (next section) and ValidatingAdmissionPolicy.

EventRateLimit (deprecated)

Rate-limits API requests to protect against runaway controllers or attackers. Disabled by default in production (the API server’s --max-requests-inflight is the modern mechanism).

DefaultStorageClass, DefaultTolerationSeconds, Priority

Defaults new PVCs to the default StorageClass; sets default toleration seconds; applies Pod priority from PriorityClass defaults.

Custom admission webhooks

For policies that built-in admission does not cover, the cluster operator configures admission webhooks. Two kinds:

  • MutatingWebhookConfiguration — runs before schema validation; can modify the object
  • ValidatingWebhookConfiguration — runs after schema validation; can only accept or reject
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: image-registry-allowlist
webhooks:
- name: registry.example.com
  sideEffects: None
  admissionReviewVersions: ["v1"]
  clientConfig:
    service:
      name: image-policy
      namespace: policy
      path: /validate
  rules:
  - operations: ["CREATE", "UPDATE"]
    apiGroups: [""]
    resources: ["pods"]
  failurePolicy: Fail
  namespaceSelector:
    matchExpressions:
    - key: kubernetes.io/metadata.name
      operator: NotIn
      values: ["kube-system", "policy"]

The webhook receives an AdmissionReview with the object, decides allow/deny, and returns. Production webhooks must be:

  • HA — at least 2 replicas; failure of one doesn’t stop admission
  • Fast — under 1 second p99; admission is on the hot path
  • Observable — metrics on admission latency, error rate, allow/deny counts
  • Auditable — logs every decision with full request context

ValidatingAdmissionPolicy (CEL-based)

Since Kubernetes 1.30, ValidatingAdmissionPolicy provides a declarative admission mechanism using CEL (Common Expression Language):

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: require-image-digest
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups: [""]
      apiVersions: ["v1"]
      operations: ["CREATE", "UPDATE"]
      resources: ["pods"]
  validations:
  - expression: "object.spec.containers.all(c, c.image.contains('@sha256:'))"
    message: "All container images must be pinned by digest (@sha256:...)"

The expression is a CEL expression evaluated against the object. If it returns false, the request is rejected with the message.

This is a powerful pattern: declarative, no webhook to operate, no failure mode from webhook outage. Production in 2026 uses ValidatingAdmissionPolicy for many policy needs where it used to require a webhook.

# Example: deny privileged containers
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: deny-privileged
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups: [""]
      apiVersions: ["v1"]
      operations: ["CREATE", "UPDATE"]
      resources: ["pods"]
  validations:
  - expression: "object.spec.containers.all(c, !has(c.securityContext) || !c.securityContext.privileged)"
    message: "Privileged containers are not allowed"

How to test admission

For webhooks, use kubectl apply --dry-run=server:

kubectl apply -f manifest.yaml --dry-run=server

The API server runs the full pipeline (authn, authz, admission) and returns the result without persisting. This is the standard way to test admission before committing changes.

For ValidatingAdmissionPolicy, the same --dry-run=server applies; the CEL evaluator runs and reports violations.

How to debug admission failures

A 422 Unprocessable Entity response is admission rejection. The response body names the webhook or built-in:

{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {},
  "status": "Failure",
  "message": "admission webhook \"deny-privileged\" denied the request: Privileged containers are not allowed",
  "code": 422
}

For built-in admission:

Error from server (Forbidden): error when creating "pod.yaml":
admission webhook "pod-security.kubernetes.io" denied the request:
pods "web-7c8" is forbidden: violates PodSecurity "restricted:latest"
...

Production debugging steps:

  1. Read the message — it names the policy that rejected.
  2. If a webhook, check its health (kubectl get pods -n policy), its recent logs, and its response time.
  3. If a built-in (PodSecurity, ResourceQuota, LimitRanger), inspect the namespace’s configuration.
  4. If failurePolicy: Fail and the webhook is timing out, either restore the service or temporarily switch to failurePolicy: Ignore to unblock the cluster.

Admission observability

Admission contributes to API server latency. The relevant metrics:

  • apiserver_admission_controller_admission_duration_seconds — admission duration by controller and operation
  • apiserver_admission_webhook_admission_duration_seconds — webhook-specific duration
  • apiserver_admission_webhook_rejection_count — count of rejections by webhook

A spike in webhook latency or rejection count is the early indicator of webhook trouble.

Cross-course references

  • The Linux course part XXIX-Linux-Hardening covers least-privilege principles that map onto admission policy.
  • The Observability course part V-Observability-PromArchitecture covers the metrics the API server exposes about admission.
  • The Linux course part XXVIII-Linux-MAC covers mandatory access controls; admission policies are the cluster’s MAC.
  • The Docker course part XXXVIII-Docker-Secrets covers secret management patterns that interact with admission (imagePullSecrets injection).

Quiz

Knowledge check · 4 questions

  1. Q1. Which built-in admission controller enforces Pod Security Standards at the namespace level?

  2. Q2. A `failurePolicy: Ignore` webhook that becomes unreachable will cause Pods to be rejected with 422.

  3. Q3. A developer reports they cannot create a new Deployment. The error is `admission webhook "resourcequota.admission.k8s.io" denied the request: exceeded quota: team-a-prod, requested: requests.cpu=16, used: 56, limited: 64`. Diagnose and remediate.

    ResourceQuota (team-a-prod): ```yaml spec: hard: pods: "200" requests.cpu: "64" requests.memory: 128Gi limits.cpu: "128" limits.memory: 256Gi ``` Current usage: ``` $ kubectl describe resourcequota team-a-prod -n team-a-prod Name: team-a-prod Used Hard ------ ---- pods 200 200 requests.cpu 56 64 requests.memory 98Gi 128Gi limits.cpu 90 128 limits.memory 180Gi 256Gi ``` The Deployment: ```yaml spec: template: spec: containers: - name: web image: nginx:1.27.1 resources: requests: cpu: 16 memory: 4Gi ``` The Deployment has 1 replica.

  4. Q4. Explain ValidatingAdmissionPolicy. When would you use it instead of a webhook?

Passing score: 75%. Answers are checked in this browser.

Production discipline

  • Run PodSecurity at restricted for new namespaces; allow older namespaces to migrate gradually.
  • Set ResourceQuota and LimitRange on every namespace. Without them, any Pod can request unbounded resources.
  • For custom policies, prefer ValidatingAdmissionPolicy for declarative rules; use webhooks only for policies that need external data.
  • Every webhook must be HA, fast, observable, and auditable. A flaky webhook blocks the cluster.
  • Test every policy with kubectl apply --dry-run=server before committing to a namespace-wide rollout.