Tools · OPA & Gatekeeper

OPA & Gatekeeper

The Open Policy Agent is a CNCF-graduated, general-purpose policy engine: you write rules in a language called Rego, hand OPA some JSON, and it answers “is this allowed?” — for Kubernetes objects, HTTP requests, Terraform plans, CI artefacts, anything. Gatekeeper is OPA wearing a Kubernetes uniform: an admission controller that runs those rules against every object entering the cluster and rejects the ones that break the rules. Together they solve the platform problem of “we have written down our standards, and nobody follows them” — turning a wiki page of shoulds into a control the API server enforces on every request, for every team, every time.

☺ Explain it like I’m 10

Imagine a big theme park with one gate. There’s a sign at the gate: “you must be this tall, you must have a ticket, no glass bottles.” Now imagine the sign can’t actually stop anyone — people just walk past it. That’s a wiki page. Gatekeeper is a real guard standing at the gate who reads the rules off a card and checks every single person before they get in. And OPA is the brain inside the guard: it doesn’t know anything about theme parks specifically — it just knows how to read a rule card and say yes or no. The clever part is that the rule cards are written down as ordinary files, so you can review them, test them, and put them in Git — instead of hoping everyone remembers.

🐢Your host for this topic: Timmy the Turtle — slow, careful, and utterly unbothered by your deadline. Timmy builds the guardrails that stop a bad manifest before it becomes a 3 a.m. page, and insists you run every new rule in dryrun first so the guardrail doesn’t become the outage.

What OPA and Gatekeeper are, and the problem they solve

☺ Like you’re 10: One is a brain that knows how to check rules. The other plugs that brain into the door of your cluster.

Every platform team eventually writes the same document: every workload must have an owner label, images must come from our registry, nothing runs as root, production namespaces need resource limits. And every platform team then discovers that a document changes nobody’s behaviour. Code review catches some of it, on the days people are paying attention; the rest arrives in production and is found later by an auditor, an incident, or a bill.

Policy as code, enforced at the door

The fix is to move the rule from prose into an executable artefact that lives on the request path. Kubernetes gives you the hook: admission webhooks. After a request has been authenticated and authorised, but before the object is persisted to etcd, the API server calls out to registered webhooks and asks “should I accept this?” A rejection there is total — it doesn’t matter whether the object came from kubectl, a Helm chart, a CI job, or an Argo CD sync. That is what makes admission control the single highest-leverage control surface on a cluster, and it is why Security & Policy spends so much time there.

OPA — the general-purpose engine

Open Policy Agent graduated from the CNCF in 2021 and is deliberately not a Kubernetes tool. It is a small binary (or a Go library) that loads policies written in Rego plus optional data, accepts an arbitrary JSON input, and returns an arbitrary JSON decision. That generality is its superpower: the same engine and the same language secure a microservice’s HTTP API (via an Envoy or service-mesh authorisation filter), gate a terraform plan in CI, lint Dockerfiles and Kubernetes YAML before they merge (through Conftest), and decide who may call which SQL procedure. Learning Rego once buys you policy everywhere, which is exactly the argument Governance & Compliance makes about controls being reusable.

Gatekeeper — OPA’s Kubernetes integration

Gatekeeper is an OPA subproject: a controller you install into the cluster (namespace gatekeeper-system) that registers itself as a validating and mutating admission webhook, then evaluates your Rego against every incoming object. Crucially, it does not ask you to hand-write webhook configurations or mount Rego files into a pod. It introduces the constraint framework: a two-object model where the platform team publishes a reusable, parameterised policy template, and then anyone can instantiate it as a native Kubernetes custom resource. Policy becomes an API object, so it flows through GitOps, RBAC, and audit like everything else.

◆ Key idea

Hold these two apart in your head and half the confusion disappears. OPA is the engine and the language (Rego), useful far beyond Kubernetes. Gatekeeper is the Kubernetes product built on it — the webhook, the audit loop, and the ConstraintTemplate/Constraint CRDs. On the exam, a question about Rego syntax is an OPA question; a question about enforcementAction or match.excludedNamespaces is a Gatekeeper question.

Where it fits in a platform

☺ Like you’re 10: It sits at the front door of the cluster, checking everything on the way in — and it also walks around inside checking things that got in earlier.

In the layered model from Platform Architecture, Gatekeeper is a security and governance plane component that physically intercepts the control plane’s write path. It is not part of the developer control plane (developers should barely know it exists — they meet it only as a helpful error message) and it is not part of the resource plane. Its job is to make the golden path from Self-Service & Golden Paths also the only path: the guardrail and the paved road are the same object.

Its neighbours

Directly beside it sits Kyverno, its main alternative — same problem, YAML instead of Rego, compared in a table further down. Underneath it is the Kubernetes admission machinery itself, covered in The Kubernetes Substrate, including built-in Pod Security Admission, which handles the pod-hardening subset natively and should carry that load so your policies don’t have to. Upstream, Trivy scans images and Sigstore/cosign signs them — but a scan and a signature only protect the cluster if something refuses unsigned or vulnerable images at admission, and that something is usually Gatekeeper or Kyverno. Alongside it, Crossplane claims and other custom resources are perfectly ordinary admission targets, so “no database without a cost-centre label” becomes a constraint rather than a code review. And Falco covers what Gatekeeper structurally cannot: Gatekeeper judges declarations at write time, Falco watches behaviour at runtime.

CNPE domain relevance

Both OPA and Gatekeeper are named on the official CNPE tool list. They land hardest in Security & Policy (15% of the exam) — policy as code, guardrails, multi-tenancy isolation — and they reach into GitOps & Continuous Delivery (25%), because constraints are manifests reconciled from Git like anything else, and into Platform APIs & Self-Service (25%), because a well-chosen constraint set is what makes self-service safe enough to offer at all. Pair this page with the security practice tasks and the exam guide.

🦆 Dot’s-eye view

“I pushed a Deployment and got back a wall of red text: admission webhook "validation.gatekeeper.sh" denied the request: [require-team-label] you must provide labels: {"team"}. My first reaction was that the platform team had broken something. My second, about forty seconds later, was that the message told me exactly what to add and I fixed it in one line. Honestly? Better than finding out in an audit six months later that my service had no owner.”

How it works — architecture, components and CRDs

☺ Like you’re 10: There’s a guard at the door, and a second guard who wanders around inside with a clipboard checking who slipped in before the rules existed.

A Gatekeeper install is two Deployments and a handful of CRDs. The controller-manager serves the webhooks and reconciles your policy objects; the separate audit pod periodically re-evaluates everything already in the cluster. That split matters, and it is the first thing to be able to explain.

🦆 request kubectl · Argo CD API server authn · authz 1 · MUTATING Assign · AssignMetadata 2 · VALIDATING constraints evaluated etcd persisted · admitted deny → 4xx with your msg · nothing is written ConstraintTemplate the Rego + the CRD schema authored once, by the platform Constraint match · parameters enforcementAction: deny | dryrun | warn 🐢 audit pod re-checks what is already in the cluster generates loaded by the webhook writes status.violations + totalViolations Webhook stops new bad objects · audit finds the bad objects that are already here

The constraint framework: template plus instance

This is the design that distinguishes Gatekeeper from “an OPA pod behind a webhook,” and it is worth stating precisely. A ConstraintTemplate does two things at once. Under spec.crd.spec.names.kind it declares the name of a brand-new Kubernetes kind, and under spec.crd.spec.validation.openAPIV3Schema it declares that kind’s parameter schema — Gatekeeper then actually generates and registers the CRD for you. Under spec.targets[].rego it carries the Rego that implements the logic. The result is that a Constraint — an instance of the generated kind — is just YAML: which objects to match, what parameters to use, and how hard to enforce. One template, many constraints.

ObjectAPI groupWho writes itWhat it carries
ConstraintTemplatetemplates.gatekeeper.sh/v1Platform team (or copied from the gatekeeper-library)The Rego, plus the schema of the CRD it generates
Constraint (e.g. K8sRequiredLabels)constraints.gatekeeper.sh/v1beta1Platform team, tenant leads, anyone with RBACmatch, parameters, enforcementAction; audit writes status.violations back
Assign / AssignMetadata / ModifySetmutations.gatekeeper.sh/v1Platform teamMutations applied before validation
ExpansionTemplateexpansion.gatekeeper.sh/v1beta1Platform team“Also evaluate the Pod this Deployment would create”
Providerexternaldata.gatekeeper.sh/v1beta1Platform teamAn HTTPS endpoint Rego may call for outside facts
Configconfig.gatekeeper.sh/v1alpha1Platform teamWhich kinds to cache for referential rules; global namespace exemptions

Matching: which objects a constraint actually touches

spec.match is where most real-world constraint behaviour lives, and where most mistakes are made. It accepts kinds (a list of apiGroups + kinds pairs, both accepting "*"), namespaces (an allow-list — only these), excludedNamespaces (a deny-list — everything but these), labelSelector (on the object), namespaceSelector (on the object’s namespace), scope (Cluster, Namespaced or *), and name with wildcard support. An empty match matches everything, which is exactly as dangerous as it sounds. In practice you almost always want to exclude kube-system, gatekeeper-system and your CNI/CSI namespaces, or you will eventually block a control-plane component from starting.

Audit, mutation, expansion and external data

Four capabilities beyond plain validation round out the picture. Audit runs on an interval (default 60 seconds) and re-evaluates every constraint against everything already in the cluster, writing results into each constraint’s status.violations list and status.totalViolations count — this is how you discover the 400 workloads that already break a rule you just wrote. Mutation lets Gatekeeper fix objects rather than only reject them: Assign sets fields anywhere in the object except under metadata, AssignMetadata handles exactly that gap — adding labels and annotations, only on CREATE, and only when the key is not already set — and ModifySet adds or removes entries in a list treated as a set. Mutating webhooks always run before validating ones, so a mutation can satisfy a constraint that would otherwise deny. ExpansionTemplate solves a genuinely annoying problem: a policy about Pods never fires when someone applies a Deployment, because the Deployment is what hits admission — expansion generates the implied Pod and evaluates policy against it too, so the user gets rejected at kubectl apply instead of silently getting a Deployment with zero ready replicas. External data lets Rego call out to a registered Provider endpoint mid-evaluation — the standard use is asking an image-signature or vulnerability service about a specific image tag.

Rego — the language inside the template

Rego is declarative and query-based, and it is unlike anything you have written before — the honest headline objection to Gatekeeper. A few ideas carry you a long way. A rule has a head and a body; the body’s expressions are implicitly ANDed, and multiple rules of the same name are implicitly ORed. Rules producing a set rather than a single value are partial rules, which is exactly what a policy wants: emit one entry per problem found, and an empty set means “all good.” Comprehensions build collections inline — {c.name | c := input.review.object.spec.containers[_]} is the set of container names, where _ is Rego’s “for each element” wildcard. Built-ins do the rest: startswith, endswith, contains, count, sprintf, regex.match, object.get, plus string, arithmetic and time helpers.

Everything Gatekeeper hands you lives under input: input.review.object is the object being admitted, input.review.oldObject its previous state on an update, input.review.operation is CREATE/UPDATE/DELETE, input.review.userInfo is who is asking, and input.parameters is whatever the Constraint passed in. The other global is data — the document tree of everything loaded into the engine, including the cached cluster objects listed in the Config resource, which appear under data.inventory. That cache is what makes referential constraints possible (“no two Ingresses may claim the same host”) and also their weakness: it is eventually consistent, so two conflicting objects created in the same instant can both slip through. Finally, Rego has a real test runner — test files are ordinary Rego with rules named test_*, run by opa test — so a policy is a unit-tested artefact in CI, not a hopeful blob pasted into a cluster.

The resources you will actually write

☺ Like you’re 10: Here is the real YAML — the rule card, the instance of the rule, and the helper that fills in a missing field for you.

A ConstraintTemplate

The canonical starting example, and the one to be able to reproduce from memory. Read the three marked lines carefully — they are where people go wrong.

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels          # MUST be the lowercase of crd.spec.names.kind
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels    # (1) the NEW Kubernetes kind Gatekeeper generates
      validation:
        openAPIV3Schema:           # (2) the schema of spec.parameters on the Constraint
          type: object
          properties:
            labels:
              type: array
              items:
                type: string
            message:
              type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels  # (3) package name — keep it matching the template

        violation[{"msg": msg}] {                      # THE convention: violation, not deny
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing  := required - provided              # set difference
          count(missing) > 0
          msg := sprintf("you must provide labels: %v", [missing])
        }
⚠ The classic trap — violation, not deny

Almost every OPA tutorial on the internet, and OPA’s own standalone Kubernetes admission examples, write deny[msg] { ... }. Gatekeeper does not look at deny. It queries a partial set rule named violation whose elements are objects with a msg key: violation[{"msg": msg}] { ... } (an optional details key may accompany it). Write deny and your template will install cleanly, your constraint will appear, audit will report zero violations, and nothing will ever be blocked — a silent failure that looks exactly like success. Keep the package name aligned with the template name too; a mismatched package is the second-most-common cause of a policy that quietly does nothing. Gatekeeper has been adding support for Rego v1 syntax, where the same rule is written violation contains {"msg": msg} if { ... }; check what your installed version accepts before switching styles — the rule name is unchanged either way.

The Constraint it generates

Now the template is installed, K8sRequiredLabels is a real kind and you can instantiate it as many times as you like with different scopes and parameters.

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels                 # the kind the template created
metadata:
  name: ns-must-have-owner
spec:
  enforcementAction: deny               # deny (default) | dryrun | warn
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
    excludedNamespaces: ["kube-system", "gatekeeper-system", "kube-public"]
  parameters:
    labels: ["owner", "cost-centre"]
---
# A SECOND constraint from the SAME template — different scope, softer enforcement.
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: workloads-should-have-team
spec:
  enforcementAction: dryrun             # record violations in status; block nothing
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds: ["Deployment", "StatefulSet", "DaemonSet"]
    namespaceSelector:
      matchLabels:
        environment: production         # only production namespaces
  parameters:
    labels: ["team"]
enforcementActionWhat the user experiencesWritten to status.violations?Use it when
deny (default)Request rejected with your msg; nothing is persistedYes (audit also reports pre-existing offenders)The rule is agreed, the backlog is clean, and you mean it
dryrunNothing at all — the request succeeds silentlyYesEvery single new policy, for its first week. Measure the blast radius before you arm it
warnRequest succeeds, but kubectl prints a warningYesThe migration step between dryrun and deny — visible nudge, no breakage

A mutation that fixes the problem instead of rejecting it

Denial is not always the kindest control. If every pod in a tenant namespace must carry a seccompProfile and a billing label, adding them is friendlier than refusing. Mutations run before validation, so the mutated object is what gets judged.

apiVersion: mutations.gatekeeper.sh/v1
kind: AssignMetadata                       # metadata.labels / metadata.annotations ONLY
metadata:
  name: add-billing-label
spec:
  match:
    scope: Namespaced
    kinds:
      - apiGroups: ["*"]
        kinds: ["Pod"]
    excludedNamespaces: ["kube-system", "gatekeeper-system"]
  location: "metadata.labels.billing"
  parameters:
    assign:
      value: "platform-shared"             # applied on CREATE only
---
apiVersion: mutations.gatekeeper.sh/v1
kind: Assign                               # any field, via a typed location path
metadata:
  name: default-seccomp
spec:
  applyTo:
    - groups: [""]
      versions: ["v1"]
      kinds: ["Pod"]
  match:
    scope: Namespaced
    excludedNamespaces: ["kube-system", "gatekeeper-system"]
  location: "spec.containers[name:*].securityContext.seccompProfile.type"
  parameters:
    pathTests:
      - subPath: "spec.containers[name:*].securityContext.seccompProfile.type"
        condition: MustNotExist            # never overwrite an explicit choice
    assign:
      value: RuntimeDefault

Testing the policy before it reaches a cluster

Policy is code, so it gets tests. Here is Rego’s own test style plus the Gatekeeper-specific harness. The snippet below is written in the same classic (v0) Rego style as the template above; OPA 1.0 and later parse Rego v1 by default, so with a current opa binary you either add the if keyword to each rule head or run the binary in its v0-compatibility mode.

# labels_test.rego — run with:  opa test . -v
package k8srequiredlabels

test_missing_label_is_a_violation {
  result := violation with input as {
    "review": {"object": {"metadata": {"labels": {"app": "checkout"}}}},
    "parameters": {"labels": ["owner"]}
  }
  count(result) == 1
}

test_all_labels_present_is_allowed {
  result := violation with input as {
    "review": {"object": {"metadata": {"labels": {"app": "checkout", "owner": "payments"}}}},
    "parameters": {"labels": ["owner"]}
  }
  count(result) == 0
}
◆ Key idea · shift the guardrail left

An admission denial at kubectl apply is late feedback — the developer already wrote the manifest, opened the PR, merged it, and watched the sync fail. Run the same policies earlier: gator test or conftest against the rendered manifests in CI, so the pull request goes red instead of the deployment. Gatekeeper then becomes the backstop that catches what bypassed CI, not the primary feedback channel. That two-place pattern — fast check in the pipeline, authoritative check at admission — is the shape Best Practices recommends for every guardrail.

Day-to-day commands

☺ Like you’re 10: A short list of things you type to install it, see what it is blocking, and find out why it isn’t.

Install, and see the policy inventory

helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm install gatekeeper gatekeeper/gatekeeper \
  --namespace gatekeeper-system --create-namespace

kubectl get pods -n gatekeeper-system        # controller-manager (x3) + audit (x1)

# What policy exists on this cluster? The two-line survey:
kubectl get constrainttemplates                       # the rule cards
kubectl get constraints                               # EVERY constraint of every kind
kubectl get k8srequiredlabels ns-must-have-owner -o yaml

# Is a template healthy? A Rego compile error shows up HERE, not at apply time.
kubectl get constrainttemplate k8srequiredlabels \
  -o jsonpath='{.status.byPod[*].errors}'

Find out what is currently in violation

# The single most useful command on this page — the audit results.
# Name a single constraint by its OWN generated kind, not by the "constraints" category:
kubectl get k8srequiredlabels ns-must-have-owner \
  -o jsonpath='{.status.totalViolations}'
kubectl describe k8srequiredlabels ns-must-have-owner   # the violation list, with names

# Cluster-wide violation sweep before flipping anything to deny:
kubectl get constraints -o json \
  | jq -r '.items[] | "\(.kind)/\(.metadata.name): \(.status.totalViolations)"'

# Why did that apply fail? The message is in the API server's response — read it.
kubectl apply -f deploy.yaml
# Error from server (Forbidden): admission webhook "validation.gatekeeper.sh"
#   denied the request: [ns-must-have-owner] you must provide labels: {"owner"}

kubectl logs -n gatekeeper-system -l control-plane=controller-manager --tail=100
kubectl logs -n gatekeeper-system -l control-plane=audit-controller  --tail=100

The OPA and gator CLIs

# Plain OPA — the engine, with no Kubernetes involved at all
opa fmt -w policy.rego                 # format
opa check policy.rego                  # compile / type-check
opa test . -v                          # run every test_* rule
opa eval -i input.json -d policy.rego 'data.k8srequiredlabels.violation'
opa run --server                       # REST API: POST /v1/data/<package>/<rule>

# gator — Gatekeeper's own harness: templates + constraints + manifests together
gator test --filename=policy/ --filename=manifests/     # ad hoc evaluation
gator verify ./suites/...                               # run declarative Suite tests
gator expand --filename=deployment.yaml                 # show the implied Pod

Fold the ones you use daily into your own command reference.

Gotchas and failure modes

☺ Like you’re 10: Three ways this goes wrong: the rule secretly does nothing, the rule blocks everything, or the guard falls asleep and the whole cluster stops.

The policy that silently does nothing

By far the most common outcome for a first-time Gatekeeper user, and it always looks like success. The causes, in order of frequency: the rule is named deny instead of violation; the package name in the Rego doesn’t match; the Rego failed to compile and the error is sitting in status.byPod[].errors on the ConstraintTemplate where nobody looked; enforcementAction: dryrun was left on from the rollout; or match simply doesn’t select the objects you think it does — often because you wrote a Pod policy and everyone deploys Deployments (that is what ExpansionTemplate is for). The discipline: after installing any constraint, prove it works by applying a deliberately bad object and confirming the rejection. An untested guardrail is worse than none, because you have stopped worrying.

Audit-only forever

The other failure is organisational rather than technical. Starting in dryrun is correct and Timmy insists on it. But dryrun is comfortable: nobody complains, the dashboard shows a satisfying violation count, and the count never reaches zero because nothing forces it to. Two years later you have thirty policies, none of which block anything, and a compliance story built entirely on a number in a status field. Give every dryrun constraint an owner and a date on which it flips to warn and then deny, drive the violation backlog down deliberately, and treat a constraint that has been in dryrun for six months as the anti-pattern it is.

Webhook timeouts, availability and the day Gatekeeper takes the cluster down

An admission webhook sits on the critical path of every write to the cluster, which makes it a genuine availability risk. Three things to get right. failurePolicy: Ignore admits requests when Gatekeeper is unreachable (the cluster keeps working, enforcement has a hole); Fail rejects them (airtight enforcement, and if Gatekeeper is down nothing deploys — potentially including Gatekeeper’s own recovery). Gatekeeper ships its validating webhook with Ignore for exactly this reason; hardening to Fail is a deliberate decision that must come with multiple replicas, a PodDisruptionBudget, and system-namespace exemptions. Timeouts: timeoutSeconds is small (Gatekeeper defaults to 3), and expensive Rego — deep loops, huge comprehensions, external-data calls to a slow endpoint — will blow through it under load. Keep policies cheap and test them at realistic object sizes. Exemptions: never let a constraint match kube-system or gatekeeper-system. When the symptom is “everything suddenly fails to deploy,” check kubectl get validatingwebhookconfiguration early — see delivery triage, workload triage and the troubleshooting playbook.

🐢 Timmy’s workshop · 20 min

On a throwaway kind cluster: install Gatekeeper with Helm and wait for all pods to be Ready. Apply the k8srequiredlabels ConstraintTemplate above, then confirm the new kind exists with kubectl get crd k8srequiredlabels.constraints.gatekeeper.sh. Apply the ns-must-have-owner constraint with enforcementAction: dryrun, wait one audit interval, and read status.totalViolations — every namespace you already have is an offender. Now flip it to deny and try kubectl create ns test; read the rejection message. Then break it on purpose: rename the Rego rule from violation to deny, re-apply, and watch the constraint report zero violations and block nothing at all — that silence is the trap, and now you have felt it. Finally, add the AssignMetadata mutation and confirm a fresh pod comes out with the billing label it never asked for.

Alternatives and when to choose it

☺ Like you’re 10: Other guards can stand at the same door — here is how to pick one.

The real question is not “which is best” but “how much policy power do I need, and who on my team will maintain it?”

The comparison that decides it

OptionHow you express policyBest whenCosts you
OPA / GatekeeperRego inside a ConstraintTemplate; constraints are generated CRDsYou want one policy language across Kubernetes, CI, Terraform and service APIs; you need genuinely complex logic; you value the parameterised template-plus-instance modelRego is a real learning curve and a bus-factor risk; mutation and generation feel bolted on; two objects to ship for one rule
KyvernoYAML ClusterPolicy resources with validate, mutate, generate, verifyImagesYour team is fluent in Kubernetes YAML and not in a new language; you want mutation and resource generation as first-class features; you want image-signature verification built inComplex conditions get verbose; Kubernetes-only, so nothing transfers to CI or API authorisation
ValidatingAdmissionPolicy (built into Kubernetes, CEL)CEL expressions in a native ValidatingAdmissionPolicy + bindingSimple field-level checks you want with zero extra components and no webhook on the request pathCEL is deliberately limited; validation only — mutation lives in a separate, much newer MutatingAdmissionPolicy API; far smaller ecosystem of ready-made rules
Pod Security AdmissionNamespace labels — pod-security.kubernetes.io/<mode>: <level>, where mode is enforce, audit or warn and level is privileged, baseline or restrictedPod hardening specifically — it is built in, free, and needs no controllerOnly covers the pod-security dimension; no custom rules at all
Conftest / opa eval in CIThe same Rego, run against files before mergeFast developer feedback and gating Terraform plans, Dockerfiles and Helm outputAdvisory only — anything applied outside the pipeline bypasses it entirely
KubewardenPolicies compiled to WebAssembly, in any languageYou want to write policy in a general-purpose language (Rust and Go have the most mature SDKs) and distribute it as OCI artefactsSmaller community; another runtime concept to learn

A practical rule, and the gatekeeper-library

Choose Gatekeeper when policy is a first-class engineering discipline for you — when you already have, or want, Rego running in CI and at service-mesh authorisation, and when the ability to publish one parameterised template that ten teams instantiate differently is worth the language cost. Choose Kyverno when your policy needs are Kubernetes-shaped and your team’s fluency is in YAML; it is the lower-friction default for most platforms, and there is no shame in that. Choose neither first: turn on Pod Security Admission before you write a single custom rule, because it is free and covers the most common asks. And whichever you pick, do not start from a blank file — the gatekeeper-library is the upstream collection of ready-made ConstraintTemplates (required labels, allowed repositories, replica limits, disallowed capabilities, the full Pod Security Policy replacement set) that you copy, parameterise, and ship. See The Tool Landscape for where all of these sit, and IaC & Control Planes for policy over infrastructure rather than workloads.

🎬 At the Platform Guild
🦊

Foxy: We wrote the standards on the wiki. Isn’t that basically policy as code?

🐢

Timmy: It’s policy as prose. Nothing reads it at 4 p.m. on a Friday. A ConstraintTemplate gets read on every single request, forever, and never gets tired.

👺

Gizmo: Fine — I wrote one! deny[msg] { ... }. Applied it an hour ago. Zero violations across the whole fleet. We’re clean! 🤑

🐢

Timmy: You’re not clean, Gizmo, you’re invisible. Gatekeeper only queries violation. Your deny rule compiled beautifully and has never been asked a question in its life.

🦆

Dot: Please don’t switch it all to deny at once. Half my namespaces predate the label rule.

🐢

Timmy: That’s exactly why we start at dryrun and read status.totalViolations first. Then warn. Then deny, on a date we agree in advance — with your backlog burned down before the date, not after.

🦊

Foxy: And if Gatekeeper itself falls over?

🐢

Timmy: Then failurePolicy: Ignore means we keep deploying with a hole in the fence, and Fail means nobody deploys anything at all. Pick on purpose, exempt the system namespaces either way, and never let one replica be the whole guard.

Exam relevance and going further

☺ Like you’re 10: On exam day you can’t open OPA’s website — so the shape of a ConstraintTemplate has to already be in your head.

OPA and Gatekeeper both appear on the official CNPE tool list. Expect to be asked to read a ConstraintTemplate and say what it blocks, write or repair a Constraint (its match, parameters and enforcementAction), explain the difference between the webhook and the audit loop, diagnose a policy that isn’t firing, and articulate — in words — how Gatekeeper differs from Kyverno. The concepts sit inside Security & Policy; drill them with the security practice tasks.

The documentation allowlist — read this twice

⚠ OPA’s and Gatekeeper’s own docs are not available during the exam

During the CNPE the only documentation you may open is kubernetes.io/docs, kubernetes.io/blog, task-specific documentation explicitly linked in the exam’s Quick Reference box, and local man pages and /usr/share docs on the exam machine. openpolicyagent.org and open-policy-agent.github.io/gatekeeper are not on that list. Unless a task’s Quick Reference hands you a link, you write the ConstraintTemplate and Constraint from memory. What does work offline: kubectl explain constrainttemplate.spec, kubectl explain k8srequiredlabels.spec.match, kubectl get constraints, and kubectl get crd <name> -o yaml against whatever is already installed on the exam cluster — plus kubernetes.io/docs for the generic admission-webhook concepts. Drill the manifest shapes on Know Cold; that page exists precisely for the YAML you cannot look up.

⚖ CNPA vs CNPE — That allowlist is a CNPE-only mechanic: CNPE is hands-on, so it grants a narrow set of live lookups mid-task. CNPA is stricter, not looser — a fully closed-book multiple-choice exam with zero external resources and zero lookups of any kind, not even kubernetes.io. Even so, the concept layer above — what OPA and Gatekeeper each are, the ConstraintTemplate/Constraint split, and how deny/dryrun/warn differ — is exactly the kind of concept-level knowledge that pays off in CNPA's closed-book recall.

What to be able to do without notes

Say in one sentence what OPA is and what Gatekeeper adds. Write a ConstraintTemplate from a blank file: apiVersion: templates.gatekeeper.sh/v1, spec.crd.spec.names.kind, spec.crd.spec.validation.openAPIV3Schema, and spec.targets[0].target: admission.k8s.gatekeeper.sh with the Rego under rego:. Write the rule head as violation[{"msg": msg}] — never deny — and keep the package name aligned. Write the matching Constraint under constraints.gatekeeper.sh/v1beta1 with match.kinds, match.excludedNamespaces, parameters, and the right enforcementAction. Know that dryrun records and blocks nothing, warn nudges, deny rejects. Know where violations appear (status.violations, written by the audit pod, not by the webhook). Name what Assign/AssignMetadata and ExpansionTemplate are for. And be able to explain the failurePolicy trade-off out loud, because it is the question that separates “installed a tool” from “ran a platform.”

Official resources for after the exam

Outside the exam, start at openpolicyagent.org/docs (the Rego Policy Language chapter repays a slow read, and the interactive Rego Playground is the fastest way to learn the language), then the Gatekeeper documentation at open-policy-agent.github.io/gatekeeper, the ready-made policies in the gatekeeper-library, the source at github.com/open-policy-agent/gatekeeper, and the CNCF project page at cncf.io/projects/open-policy-agent-opa. Then pair this page with Security & Policy for the wider control set, Governance & Compliance for turning constraints into audit evidence, Platform APIs & Operators for the CRD machinery a ConstraintTemplate quietly uses, and the glossary whenever a term stops making sense.

🐢 Timmy’s checkpoint

1. In one sentence each, what is OPA and what does Gatekeeper add? 2. What are the two objects in the constraint framework, and what does each contain? 3. You wrote a policy, it installed cleanly, and it blocks nothing — name the three things you check first. 4. What do deny, dryrun and warn each do as an enforcementAction, and where do violations get recorded? 5. Which component writes status.totalViolations — the webhook or the audit pod? 6. Your Pod policy never fires because everyone deploys Deployments. What fixes it? 7. What is the trade-off between failurePolicy: Ignore and Fail? 8. During the exam, where do you look up the ConstraintTemplate schema?

Check your answers
  1. OPA is a general-purpose, CNCF-graduated policy engine that evaluates rules written in Rego against arbitrary JSON input — useful for Kubernetes, HTTP APIs, Terraform and CI alike. Gatekeeper is its Kubernetes integration: a validating and mutating admission controller plus an audit loop, which exposes policy as native CRDs.
  2. The ConstraintTemplate holds the Rego and defines a new CRD (via crd.spec.names.kind and validation.openAPIV3Schema). The Constraint is an instance of that generated kind, carrying match, parameters and enforcementAction.
  3. (a) Is the rule named violation[{"msg": msg}] rather than deny? (b) Does the Rego package name match, and did the template compile — check status.byPod[].errors on the ConstraintTemplate. (c) Does match actually select those objects, and is enforcementAction still dryrun?
  4. deny rejects the request with your message; dryrun allows it and blocks nothing; warn allows it but returns a warning to the client. All three record violations in the constraint’s status.violations / status.totalViolations.
  5. The audit pod — a separate Deployment that re-evaluates all existing cluster objects on an interval (60 s by default). The webhook only decides individual incoming requests; it does not populate status.
  6. An ExpansionTemplate, which generates the Pod the Deployment would create and evaluates policy against it too — so the rejection happens at kubectl apply instead of appearing later as a Deployment with zero ready replicas.
  7. Ignore admits requests when Gatekeeper is unreachable — the cluster keeps working but enforcement has a hole. Fail rejects them — enforcement is airtight, but a Gatekeeper outage stops all deployments, potentially including Gatekeeper’s own recovery. Fail requires multiple replicas, a PodDisruptionBudget, and system-namespace exemptions.
  8. You can’t — neither openpolicyagent.org nor the Gatekeeper docs site is on the exam allowlist (kubernetes.io/docs, kubernetes.io/blog, task-specific Quick Reference links, and local man//usr/share docs only). Write it from memory and use kubectl explain plus kubectl get crd -o yaml against the cluster’s installed CRDs. Drill it on Know Cold.