Safeguard
Containers

K8s Admission Controllers: Enforcing Policy at the Kubernetes API

A k8s admission controller intercepts every request to the API server and can validate or mutate it, making it the natural enforcement point for security policy.

Yukti Singhal
Platform Engineer
6 min read

A k8s admission controller is code that intercepts requests to the Kubernetes API server after authentication and authorization but before an object is persisted, letting you validate or mutate that object against your policy. Because every create, update, and delete flows through it, a k8s admission controller is the single most effective place to enforce security rules across an entire cluster. If you want to guarantee that no privileged pod ever runs, no unsigned image is ever deployed, and no container ever mounts the host filesystem, this is where you make that guarantee.

Where admission control sits in the request flow

When you run kubectl apply, the API server processes the request in a fixed order:

  1. Authentication decides who you are.
  2. Authorization (RBAC) decides whether you may perform the action.
  3. Admission control decides whether the specific object is acceptable, and can modify it.
  4. The object is validated against its schema and written to etcd.

Admission controllers run in step 3, and they come in two flavors that fire in sequence. Mutating admission runs first and can change the object, for example injecting a sidecar or setting a default security context. Validating admission runs second and can only accept or reject. The ordering matters: mutation happens before validation, so a mutating controller can fix an object that a validating controller would otherwise reject.

Built-in versus dynamic controllers

Kubernetes ships with a set of compiled-in admission controllers enabled through the API server's --enable-admission-plugins flag. NamespaceLifecycle, ResourceQuota, and LimitRanger are examples you are already relying on. These are fixed in behavior and configured through the API server.

The powerful, extensible mechanism is the dynamic admission controller, implemented as a webhook. You register a ValidatingWebhookConfiguration or MutatingWebhookConfiguration, and the API server calls out to your HTTPS endpoint with the object under review. Your endpoint returns an AdmissionReview response allowing or denying the request. This is how policy engines plug in without you writing Go and recompiling the API server.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: require-non-root
webhooks:
  - name: require-non-root.example.com
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["pods"]
    clientConfig:
      service:
        name: policy-webhook
        namespace: security
        path: /validate
    admissionReviewVersions: ["v1"]
    sideEffects: None
    failurePolicy: Fail

The failurePolicy: Fail setting is a security decision in itself: if the webhook is unreachable, requests are rejected rather than allowed through. Choosing Ignore there is a common way teams accidentally create a bypass.

Policy engines you do not have to build

Writing and operating your own webhook server is possible but rarely worth it. Two mature engines cover almost every use case.

OPA Gatekeeper uses the Rego policy language and a ConstraintTemplate and Constraint model. It is expressive and well suited to complex, cross-resource logic, at the cost of Rego's learning curve.

Kyverno writes policies as Kubernetes YAML resources, which most teams find far easier to read and maintain. A policy that blocks privileged containers looks like this:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-privileged
spec:
  validationFailureAction: Enforce
  rules:
    - name: privileged-containers
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "Privileged containers are not allowed."
        pattern:
          spec:
            containers:
              - =(securityContext):
                  =(privileged): "false"

Both engines can run in audit mode first, reporting violations without blocking, which is how you should always roll out a new policy. Turn on enforcement only after you have seen what would have been rejected.

What to enforce first

The highest-value policies map to the ways clusters actually get compromised:

  • Reject privileged containers and containers requesting host namespaces or the host filesystem.
  • Require runAsNonRoot and a dropped capability set, aligning with the practices in our guide on how to secure a Docker container.
  • Require images from trusted registries only, and ideally require signature verification so an attacker cannot swap in a tampered image.
  • Block latest tags so deployments are reproducible and pinned.
  • Enforce resource limits so one workload cannot starve a node.

Image provenance is where admission control meets supply chain security. A mutating or validating webhook can verify a Sigstore signature or check an image digest against an allowlist before the pod is ever scheduled, which is the enforcement point for everything a scanner told you at build time. Tying admission decisions to scan results, so an image with an unresolved critical CVE is simply not admitted, closes the loop between finding a vulnerability and preventing it from running.

Operating admission controllers safely

Admission webhooks sit in the critical path of every API request, so they carry operational risk. A misconfigured webhook with failurePolicy: Fail and no available backend can lock you out of your own cluster, unable to create anything, including the pods that would fix the webhook. Guard against this:

  • Scope webhooks with namespaceSelector and objectSelector so system namespaces are exempt.
  • Run the policy engine highly available so a single pod failure does not halt admissions.
  • Keep a break-glass path, such as an exempt namespace or the ability to delete the webhook configuration directly.
  • Manage the TLS certificates the API server uses to call the webhook, because an expired cert silently breaks enforcement.

Treat the admission layer as production infrastructure with its own runbook, not a fire-and-forget YAML file.

FAQ

What is a k8s admission controller?

It is code that intercepts requests to the Kubernetes API server after authentication and authorization, and can validate or mutate the object before it is stored. It is the primary enforcement point for cluster-wide security policy.

What is the difference between validating and mutating admission?

Mutating admission runs first and can change the incoming object, such as injecting defaults or sidecars. Validating admission runs second and can only accept or reject. This ordering lets a mutating controller fix an object before it is validated.

Should I use OPA Gatekeeper or Kyverno?

Kyverno writes policies as native Kubernetes YAML and is easier for most teams to adopt. OPA Gatekeeper uses Rego and handles more complex cross-resource logic. Both support audit-before-enforce rollouts.

Can an admission controller lock me out of my cluster?

Yes. A webhook with failurePolicy: Fail and no reachable backend can reject all requests, including the ones needed to fix it. Scope webhooks away from system namespaces, run the engine highly available, and keep a break-glass path.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.