Enforce Kubernetes Policies with OPA Gatekeeper
Quick answer
Install Gatekeeper, write a real ConstraintTemplate in Rego, scope it with a Constraint, and roll it out safely with dry-run mode before it ever rejects a real Pod.
- Step 1: Install Gatekeeper
- Step 2: Write a ConstraintTemplate
- Step 3: Create a Constraint
- Step 4: Test the Rejection
- Step 5: Roll Out Safely with Dry-Run Mode
intermediate · 55 min
Before you begin
- A cluster you can use (kind or minikube is fine) and kubectl configured
- Helm installed
- Basic familiarity with Kubernetes admission control concepts
- No Rego experience required — this tutorial teaches enough to be productive
OPA Gatekeeper and Kyverno solve the same problem — blocking or mutating Kubernetes resources at admission time — in different ways. Gatekeeper is the CNCF-graduated original: policies are written in Rego, OPA's purpose-built policy language, which is more expressive for complex logic but has a steeper learning curve than plain YAML. This tutorial is for readers who've chosen the Rego path, or need to evaluate it against Kyverno's YAML-based approach covered in Enforcing Policy with Kyverno.
Gatekeeper itself is a validating (and optionally mutating) admission webhook — if you want the raw mechanics of how that webhook plumbing works under the hood, Write a Kubernetes Admission Webhook From Scratch covers it directly; this tutorial won't re-derive it.
What You'll Build
- Gatekeeper installed via its official Helm chart
- A ConstraintTemplate written in Rego that requires a
teamlabel on Pods - A Constraint that applies that template to the
workloadsnamespace only - A confirmed rejection of a non-compliant Pod, and acceptance of a compliant one
- The same policy rolled out safely first, using dry-run mode
- A pointer to the community Gatekeeper policy library so you're not hand-writing everything
Step 1: Install Gatekeeper
Install via the official Helm chart:
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update
helm install gatekeeper gatekeeper/gatekeeper \
--namespace gatekeeper-system \
--create-namespaceConfirm the controller and audit pods are running:
kubectl -n gatekeeper-system get podsGatekeeper is built around two CRDs, and the split between them is the core concept to understand before writing anything:
- ConstraintTemplate — defines a reusable policy, written in Rego, with a schema for its input parameters. Think of it as a function: it doesn't do anything on its own until you instantiate it.
- Constraint — an instance of a ConstraintTemplate, scoped to specific resource kinds and namespaces, with specific parameter values filled in.
One ConstraintTemplate — "require these labels" — can back many Constraints: one requiring team on Pods in workloads, another requiring cost-center on Deployments in billing. You write the Rego logic once and reuse it by parameterizing Constraints, the same way you'd write one function and call it with different arguments instead of copy-pasting the function body.
Step 2: Write a ConstraintTemplate
Create required-labels-template.yaml. This defines a policy that rejects any object missing one or more labels named in its labels parameter:
1# required-labels-template.yaml
2apiVersion: templates.gatekeeper.sh/v1
3kind: ConstraintTemplate
4metadata:
5 name: k8srequiredlabels
6spec:
7 crd:
8 spec:
9 names:
10 kind: K8sRequiredLabels
11 validation:
12 openAPIV3Schema:
13 type: object
14 properties:
15 labels:
16 type: array
17 items:
18 type: string
19 targets:
20 - target: admission.k8s.gatekeeper.sh
21 rego: |
22 package k8srequiredlabels
23
24 violation[{"msg": msg}] {
25 provided := {label | input.review.object.metadata.labels[label]}
26 required := {label | label := input.parameters.labels[_]}
27 missing := required - provided
28 count(missing) > 0
29 msg := sprintf("you must provide labels: %v", [missing])
30 }Walking through the Rego: provided is the set of label keys actually present on the object being reviewed (input.review.object is the raw resource, exactly as it would be admitted). required is the set of label keys the Constraint asks for, read from input.parameters.labels. missing is a set difference — every required label not in the provided set. If missing is non-empty, the violation rule fires and Gatekeeper rejects the request with msg.
kind: K8sRequiredLabels is the name of the CRD this template generates — that's what you'll reference when you create Constraints. spec.crd.spec.validation.openAPIV3Schema defines what a Constraint's parameters field is allowed to contain; here, just a labels array of strings.
Apply it:
kubectl apply -f required-labels-template.yamlGatekeeper's controller reads this and dynamically registers the K8sRequiredLabels CRD — check it exists:
kubectl get crd k8srequiredlabels.constraints.gatekeeper.shStep 3: Create a Constraint
The template alone enforces nothing. Create a Constraint that instantiates it, scoped to Pods in the workloads namespace, requiring the team label specifically:
1# require-team-label.yaml
2apiVersion: constraints.gatekeeper.sh/v1beta1
3kind: K8sRequiredLabels
4metadata:
5 name: pods-must-have-team-label
6spec:
7 enforcementAction: deny
8 match:
9 kinds:
10 - apiGroups: [""]
11 kinds: ["Pod"]
12 namespaces:
13 - "workloads"
14 parameters:
15 labels: ["team"]match.kinds restricts this Constraint to core-group Pods — Gatekeeper won't even evaluate the Rego for anything else. match.namespaces scopes it to workloads only; if you wanted to enforce cluster-wide except for system namespaces, you'd use excludedNamespaces: ["kube-system", "gatekeeper-system"] instead. enforcementAction: deny is what actually blocks the request — you'll change this to dryrun in Step 5.
Apply it:
kubectl create namespace workloads
kubectl apply -f require-team-label.yamlStep 4: Test the Rejection
Try to create a Pod without the team label:
kubectl run no-label-pod --image=nginx -n workloadsGatekeeper rejects it at admission time, before it ever reaches etcd:
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request: [pods-must-have-team-label] you must provide labels: {"team"}
Now create one with the label:
kubectl run has-label-pod --image=nginx -n workloads --labels="team=platform"This one is admitted normally:
pod/has-label-pod created
Same Constraint, same Rego, different outcome based purely on whether the required label is present — that's the policy doing exactly what it says.
Step 5: Roll Out Safely with Dry-Run Mode
Setting enforcementAction: deny straight away on a real cluster is how you find out — at 2am — that half your CI pipeline creates Pods without labels. Before enforcing anything against production traffic, switch the Constraint to dry-run mode, which evaluates every matching request and records violations without blocking anything:
1# require-team-label.yaml
2apiVersion: constraints.gatekeeper.sh/v1beta1
3kind: K8sRequiredLabels
4metadata:
5 name: pods-must-have-team-label
6spec:
7 enforcementAction: dryrun # was: deny
8 match:
9 kinds:
10 - apiGroups: [""]
11 kinds: ["Pod"]
12 namespaces:
13 - "workloads"
14 parameters:
15 labels: ["team"]kubectl apply -f require-team-label.yamlIn dry-run mode, creating no-label-pod again succeeds — nothing is blocked. But Gatekeeper's audit controller still evaluates the Constraint against existing cluster state on a periodic interval and records what would have been rejected in the Constraint's own status:
kubectl get k8srequiredlabels pods-must-have-team-label -o yamlLook at status.violations:
1status:
2 auditTimestamp: "2026-08-16T10:04:22Z"
3 totalViolations: 1
4 violations:
5 - kind: Pod
6 name: no-label-pod
7 namespace: workloads
8 message: 'you must provide labels: {"team"}'This is the single most useful workflow for adopting Gatekeeper on a real cluster: leave every new Constraint in dryrun, let the audit controller run for a few cycles (the default audit interval is 60 seconds, configurable via the --audit-interval flag on the controller), review status.violations across your fleet, fix or exempt what shows up, and only then flip enforcementAction to deny. Skipping straight to deny is how a policy rollout becomes an incident.
Step 6: Don't Hand-Write Everything — Use the Policy Library
The K8sRequiredLabels template above is a reasonable first policy to write yourself because it's small enough to fully understand. For anything beyond that — requiring resource limits, blocking privileged containers, restricting image registries to an allowlist — start from the community-maintained Gatekeeper policy library instead of writing Rego from scratch. It ships ConstraintTemplates for the policies almost every cluster ends up needing, already tested against Gatekeeper's own CI, with example Constraints you adapt rather than invent. Reach for hand-written Rego, like the template above, only when a policy is specific enough to your org that nothing in the library covers it.
Verify It Worked
Confirm both CRDs are registered and active:
kubectl get constrainttemplates
kubectl get constraintsYou should see k8srequiredlabels in the first list and pods-must-have-team-label (kind K8sRequiredLabels) in the second. Set enforcement back to deny — edit require-team-label.yaml from Step 5, flipping enforcementAction back:
# require-team-label.yaml
spec:
enforcementAction: deny # back from dryrunThen re-run the Step 4 rejection to reconfirm it's actually blocking, not just auditing:
kubectl apply -f require-team-label.yaml
kubectl run no-label-pod-2 --image=nginx -n workloads
# admission webhook "validation.gatekeeper.sh" denied the requestIf that command is rejected with the same you must provide labels message, the ConstraintTemplate, the Constraint, and enforcement are all wired correctly end to end.
Where to Go Next
- Compare against the YAML-based alternative — Enforcing Policy with Kyverno covers the same admission-control problem without Rego.
- Understand the mechanism underneath — Write a Kubernetes Admission Webhook From Scratch builds the raw webhook Gatekeeper is layered on top of.
- Read the conceptual background — Kubernetes Admission Webhooks and OPA Gatekeeper covers how the pieces fit together.
- For pod security specifically — Pod Security Admission is the narrower, built-in alternative with no Rego and a fixed policy set, useful when Gatekeeper's flexibility is more than you need.
We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.
Struggling with this in production?
We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.