CKSKubernetes 1.35

Lessons

OPA Gatekeeper: a policy that survives review

updated 2026-10-22 by seed

The two objects

  • ConstraintTemplate — defines a new kind and the Rego that evaluates it.
  • Constraint — an instance of that kind: parameters, and what it matches.

Install (if the cluster does not have it):

kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.15/deploy/gatekeeper.yaml
kubectl -n gatekeeper-system get pods

Template: require a label on every Pod

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata: {name: k8srequiredlabels}
spec:
  crd:
    spec:
      names: {kind: K8sRequiredLabels}
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels: {type: array, items: {type: string}}
  targets:
  - target: admission.k8s.gatekeeper.sh
    rego: |
      package k8srequiredlabels

      violation[{"msg": msg, "details": {"missing_labels": missing}}] {
        provided := {label | input.review.object.metadata.labels[label]}
        required := {label | label := input.parameters.labels[_]}
        missing := required - provided
        count(missing) > 0
        msg := sprintf("you must provide labels: %v", [missing])
      }

Constraint: apply it to Pods, require owner

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata: {name: pods-must-have-owner}
spec:
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["Pod"]
    namespaces: ["prod"]
  parameters:
    labels: ["owner"]
kubectl apply -f template.yaml && kubectl apply -f constraint.yaml
kubectl get constraint && kubectl get k8srequiredlabels
kubectl run bad --image=nginx -n prod          # rejected

Template: only these registries

    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("image '%v' comes from an untrusted registry", [container.image])
      }

Template: replica limits

      violation[{"msg": msg}] {
        input.review.object.kind == "Deployment"
        replicas := input.review.object.spec.replicas
        limit := input.parameters.replicas
        replicas > limit
        msg := sprintf("replicas %v exceeds the limit of %v", [replicas, limit])
      }

Reading a denial

Error from server ([pods-must-have-owner] you must provide labels: {"owner"}): ...

The bracketed part is the Constraint name — that is how you prove which policy
blocked you when the task asks "which policy rejected it".

Traps

  • openAPIV3Schema must describe every parameter you pass, or the Constraint is rejected.
  • Rego is a real language: violation[{"msg": ...}] is a set rule; forget the braces and nothing validates.
  • ConstraintTemplates take a few seconds to register the new kind — kubectl get crd | grep constraints until it appears.
  • match.kinds with the wrong apiGroups silently matches nothing. "" for core, apps for Deployments.
  • A Gatekeeper that is not running looks exactly like a policy that does not match: check kubectl -n gatekeeper-system get pods first.