Certified Kubernetes Security Specialist (CKS) #16 Admission control: OPA/Gatekeeper, Kyverno

In #15 Image signing: cosign, SBOM, we learned the flow of attaching a signature to an image and then verifying that signature. But to stop an unsigned image, a Pod using the latest tag, or a container running as root before the cluster ever accepts it, you need a gate that intercepts the request and inspects it. This post covers that gate — admission control — with two tools: OPA/Gatekeeper and Kyverno.

In the CKS curriculum, admission control maps to the Monitoring, Logging, and Runtime Security domain, but where Falco is a tool that detects what has already happened at runtime, admission control is a policy-enforcement tool that stops dangerous workloads from entering the cluster in the first place. Its character differs in that it’s prevention, not detection.

What is an admission controller #

The Kubernetes API server goes through several stages when it processes an incoming request. It confirms who you are with authentication, checks whether you have permission with authorization, and then in the admission control stage inspects the request once more to decide whether to actually accept it. In other words, an admission controller is a gate that steps in right before the request is stored in etcd and looks the request over.

Kubernetes ships with built-in admission controllers like NamespaceLifecycle and LimitRanger compiled in. But to enforce arbitrary user-defined policies, you need a mechanism that sends the request out to get a verdict — this is the admission webhook.

validating vs mutating webhook #

Admission webhooks split into two kinds by behavior. This distinction is a core point the exam asks about often.

KindWhat it doesExample
ValidatingAdmissionWebhookOnly validates the request. Pass or rejectReject a Pod using the latest tag
MutatingAdmissionWebhookMutates the request object. Add or modify fieldsAuto-inject a label into every Pod

The order of processing is fixed too. The API server calls mutating webhooks first to mutate the object, then calls validating webhooks to validate the final object. The reason mutation comes first and validation comes later is that validation must re-check the result of the mutation, keeping the policy consistent.

These two webhooks are merely the skeleton of “sending the request out” — what to validate and what to mutate is decided by the policy engine. The leading representatives of that policy engine are OPA/Gatekeeper and Kyverno.

OPA/Gatekeeper #

OPA (Open Policy Agent) is a general-purpose policy engine, and Gatekeeper is the component that integrates OPA into Kubernetes as an admission webhook. Installing Gatekeeper registers a ValidatingAdmissionWebhook that judges incoming requests with OPA policy.

Rego and the two-tier resources #

Gatekeeper policies are written in a declarative query language called Rego. And it’s characteristic to define policy split across two custom resources.

  • ConstraintTemplate: Defines the policy’s logic in Rego and creates a new custom resource kind that will enforce that policy.
  • Constraint: An instance of the kind that the ConstraintTemplate created, specifying which resources the policy applies to and with which parameters.

Because logic and application are separated, a ConstraintTemplate you build once can be reused as several Constraints by changing only the parameters.

Example: allow only trusted registries #

First, let’s define the logic “accept only images that start with an allowed registry” with a ConstraintTemplate.

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedrepos
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRepos
      validation:
        openAPIV3Schema:
          type: object
          properties:
            repos:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedrepos
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          satisfied := [good | repo := input.parameters.repos[_]; good := startswith(container.image, repo)]
          not any(satisfied)
          msg := sprintf("container <%v> uses an untrusted image <%v>", [container.name, container.image])
        }

Next, we create a Constraint to actually apply this policy. Here we pass which registries to allow as parameters.

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: only-trusted-registry
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    repos:
      - "registry.example.com/"

Now if you try to create a Pod using an image that doesn’t start with registry.example.com/, it’s rejected at the admission stage. The result of kubectl apply shows the exact text you specified in the violation message.

You can build a “reject if a specific label is missing” policy the same way. Gatekeeper provides a built-in ConstraintTemplate library, so for a common policy like K8sRequiredLabels, you can pull the library’s template instead of writing Rego yourself and write only a Constraint in the same form as above. In the Constraint above, change kind to the kind the library template created and parameters to the list of required labels, and it works as is.

Kyverno #

Kyverno is an alternative to OPA/Gatekeeper that writes policy using only Kubernetes YAML syntax, without Rego. You don’t need to learn a separate query language, so the barrier to entry is low. Kyverno also runs as an admission webhook, but it handles not only validate but also mutate and generate in a single tool.

FeatureWhat it does
validateReject or warn on requests that don’t match the rule
mutateAdd or modify fields on the request object
generateCreate associated resources together when a resource is created

Example: disallow the latest tag #

Kyverno policies are written as a ClusterPolicy (or a namespace-scoped Policy). A policy that rejects containers using the latest tag looks like this.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-image-tag
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "An image must have an explicit tag, and the latest tag is forbidden"
        pattern:
          spec:
            containers:
              - image: "!*:latest"

validationFailureAction: Enforce is the key that makes a violating request actually get rejected. Set this value to Audit and it records violations without rejecting. The !*:latest in pattern means “a tag that doesn’t end in latest,” which blocks both untagged images and Pods using latest.

The trusted-registry policy we built with Gatekeeper earlier, moved to Kyverno, needs no ConstraintTemplate Rego — in the same ClusterPolicy structure you change only the image value in pattern to "registry.example.com/*". What Gatekeeper required two resources and Rego to accomplish, Kyverno finishes in a single pattern line.

Example: auto-inject a label (mutate) #

A mutate policy fixes the object instead of rejecting the request. A policy that auto-attaches a label to every Pod looks like this.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-default-label
spec:
  rules:
    - name: add-team-label
      match:
        any:
          - resources:
              kinds:
                - Pod
      mutate:
        patchStrategicMerge:
          metadata:
            labels:
              team: platform

The link to enforcing signature verification #

In #15, we attached signatures to images with cosign. The gate that enforces those signatures at the cluster level is exactly admission control. Kyverno can verify cosign signatures directly at the admission stage with its verifyImages rule, rejecting images that are unsigned or signed with an untrusted key.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "registry.example.com/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKo...truncated...
                      -----END PUBLIC KEY-----

This way, the minimal-image, scan, and signing flow we built in #13–#15 is enforced as policy at admission control, the final gate. It’s a structure where the conclusion of supply chain security closes with policy enforcement.

Comparing the two tools #

ItemOPA/GatekeeperKyverno
Policy languageRego (separate learning needed)Kubernetes YAML
Policy resourcesConstraintTemplate + Constraint (two tiers)ClusterPolicy / Policy (one)
validateYesYes
mutateYes (Assign, etc.)Yes (patchStrategicMerge, etc.)
generateNoYes
Image signature verificationNeeds external integrationBuilt in with verifyImages
Barrier to entryHigh (Rego)Low
ExpressivenessVery high (general-purpose policy)Kubernetes-only but sufficient

In short, for complex policies that need Rego’s expressiveness, or an environment where you must share policy beyond Kubernetes too, it’s Gatekeeper; if you want to enforce policy quickly and easily within Kubernetes, it’s Kyverno. On the exam you don’t know which one you’ll be given, so it’s safe to know the basic structure of both tools.

Exam points #

  • Distinguish the difference and order of validating and mutating precisely. Mutating first, validating later. The order has validation re-check the mutation’s result.
  • Gatekeeper is a two-tier structure that defines logic with a ConstraintTemplate and application with a Constraint. If you make only one of the two, the policy doesn’t work.
  • Kyverno finishes with a single ClusterPolicy, and validationFailureAction: Enforce is the key that makes a violation actually get rejected. Audit only records.
  • An exam regular is the task of verifying that a given violating manifest is rejected. After applying the policy, create a Pod using the latest tag or an untrusted registry with kubectl apply, and check firsthand that the rejection message appears.
  • A problem where you find and fill in the missing resource while the policy is already deployed also comes up. If only the ConstraintTemplate exists and the Constraint is missing, the policy isn’t applied, so recall this structure and create the missing side.
  • Browsing the official docs is allowed for policy-writing syntax, so familiarizing yourself in advance with where the examples are in the Gatekeeper and Kyverno docs saves authoring time.

Wrap-up #

What this post locked in:

  • An admission controller is a gate that intercepts and inspects a request before it’s stored in etcd. Validating is validation, mutating is mutation, and mutating runs first.
  • OPA/Gatekeeper defines Rego policy split into two tiers, ConstraintTemplate and Constraint. The separation of logic and application makes reuse easy.
  • Kyverno handles validate, mutate, and generate all with only Kubernetes YAML, without Rego. It has a low barrier to entry and builds in cosign signature verification.
  • We implemented “no latest tag” and “trusted-registry restriction” in both tools, and wrapped up the exam-favorite pattern of verifying that a violating manifest is rejected.

Admission control is the final gate that enforces the supply chain security covered in #13–#15 at the cluster level. The broader use of policy engines also connects to the cluster-operations context covered in Kubernetes Advanced #3.

Next: Falco #

Everything so far has been prevention — policy enforcement that keeps dangerous workloads out. Even so, you have to detect anomalous behavior happening inside the cluster in real time.

In #17 Falco behavioral analysis, audit logs, we’ll get hands-on with how Falco works to detect anomalous behavior at runtime — shell execution, sensitive file access, unexpected network connections — with rules, how to write those rules, and how to configure and analyze Kubernetes audit logs, and wrap it all up.

X