stuff I figured out the hard way

Key points:

  • You need both RBAC and admission policy - RBAC can’t look inside the object.
  • Default to ValidatingAdmissionPolicy (GA in Kubernetes 1.30) for simple checks; nothing to install.
  • Use Kyverno for mutation, generation, image verification or reports; write new policies as ValidatingPolicy, since Kyverno 1.19 deprecates ClusterPolicy.
  • Use Gatekeeper when you already use OPA and want one Rego language across Kubernetes, Terraform and CI.
  • Roll out every policy in audit mode first, and match controllers, not only Pods.

The path of a request

Every kubectl apply, controller update and ReplicaSet-created pod goes through the same API server steps:

flowchart LR
    REQ[Request] --> AUTHN["Authentication<br/>who are you?"]
    AUTHN --> AUTHZ["Authorization (RBAC)<br/>may you do this verb<br/>on this resource?"]
    AUTHZ --> MUT["Mutating admission<br/>add defaults, sidecars, labels"]
    MUT --> SCHEMA[Schema validation]
    SCHEMA --> VAL["Validating admission<br/>is this object allowed?"]
    VAL --> ETCD[(etcd)]

RBAC vs admission policy

Use both. RBAC alone means anyone allowed to create a pod can create a privileged one.

  • RBAC: “may this user create Deployments in this namespace?” Verbs, resources, namespaces - it can’t look inside the object.
  • Admission policy: “is this Deployment acceptable?” It reads the object: memory limits set, image from our registry, running as root?

Which engine

ValidatingAdmissionPolicyKyvernoOPA Gatekeeper
InstallBuilt in (GA in Kubernetes 1.30)Extra controllerExtra controller
LanguageCEL expressionsCEL (ValidatingPolicy); YAML patterns in the older ClusterPolicyRego
ValidateYesYesYes
MutateSeparate MutatingAdmissionPolicy, newerYesYes
Generate resourcesNoYes (e.g. a default NetworkPolicy per namespace)No
Verify image signaturesNoYesThrough external data
Reports on existing resourcesNoYesYes (audit)
Use outside KubernetesNoMostly noYes - same Rego in CI with OPA or conftest
  • Default: ValidatingAdmissionPolicy for simple checks - nothing to install or keep running.
  • Use Kyverno when: you also want mutation, generation, image verification or reports on what’s already running.
  • Use Gatekeeper when: you already use OPA and want one policy language for Kubernetes, Terraform and CI.

One policy, three ways: require memory limits

ValidatingAdmissionPolicy (CEL)

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: require-memory-limits
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: ["apps"]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["deployments", "statefulsets", "daemonsets"]
  validations:
    - expression: >-
        object.spec.template.spec.containers.all(c,
          has(c.resources) && has(c.resources.limits) &&
          'memory' in c.resources.limits)
      message: "Every container must set a memory limit."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: require-memory-limits
spec:
  policyName: require-memory-limits
  validationActions: ["Deny"]   # start with ["Warn", "Audit"]
  matchResources:
    namespaceSelector:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: NotIn
          values: ["kube-system"]

Kyverno

Use ValidatingPolicy: the built-in CEL plus Kyverno’s extras. Kyverno auto-generates checks for Deployments, StatefulSets, Jobs and other pod controllers, so a Pod policy rejects a bad Deployment at kubectl apply, not later.

apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
  name: require-memory-limits
spec:
  validationActions: [Deny]   # start with [Audit]
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: [v1]
        operations: [CREATE, UPDATE]
        resources: [pods]
    namespaceSelector:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: NotIn
          values: [kube-system, kyverno]
  validations:
    - expression: >-
        object.spec.containers.all(c,
          has(c.resources) && has(c.resources.limits) &&
          'memory' in c.resources.limits)
      message: "Every container must set a memory limit."

The older ClusterPolicy with YAML patterns is still everywhere and still works, but Kyverno 1.19 marks it deprecated:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-memory-limits
spec:
  background: true
  rules:
    - name: memory-limit
      match:
        any:
          - resources:
              kinds: ["Pod"]
      exclude:
        any:
          - resources:
              namespaces: ["kube-system", "kyverno"]
      validate:
        failureAction: Enforce   # start with Audit
        message: "Every container must set a memory limit."
        pattern:
          spec:
            containers:
              - resources:
                  limits:
                    memory: "?*"

"?*" means “any non-empty value”.

OPA Gatekeeper (Rego)

A ConstraintTemplate defines the logic once; a Constraint applies it with parameters and scope. The Rego uses the classic (v0) syntax that Gatekeeper’s rego field expects.

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredmemorylimits
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredMemoryLimits
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredmemorylimits

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.limits.memory
          msg := sprintf("container %v must set a memory limit", [container.name])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredMemoryLimits
metadata:
  name: require-memory-limits
spec:
  enforcementAction: deny   # start with dryrun or warn
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    excludedNamespaces: ["kube-system"]

Gotchas

  • Pod-only policies fail late. With a Gatekeeper or VAP policy matching only Pods, the Deployment applies and the ReplicaSet silently fails to create pods (only an event shows it). Match the controllers too, or use Kyverno, which does it for you.
  • A failing webhook can stop the cluster. With failurePolicy: Fail and the engine down, nothing matching can be created - including its own replacement pods. Run them highly available, and exclude kube-system and the engine’s own namespace.
  • Enforcing without auditing first. Direct enforcement rejects workloads you didn’t know were in violation. Use warn or audit mode, read what would be rejected, fix it, then enforce.
  • Existing resources aren’t checked at admission. New policies only see new and updated objects. Use background scans or audit reports for what’s already running.
  • Mutation order is not guaranteed. Multiple mutating webhooks run in no guaranteed order. Don’t write two mutations that depend on each other.
  • Tags are not identities. image: app:1.4 can point to a different image tomorrow. Require digests, or verify signatures (Kyverno verifyImages with cosign).
  • Untested policy breaks things. Policy is code and fails like code. Keep it in Git, deploy it through GitOps, and test it in CI with the Kyverno CLI, gator for Gatekeeper, or a dry-run cluster.