OPA and Gatekeeper¶
Who this page is for: in plain English, Gatekeeper is a gatekeeper - it sits in front of the Kubernetes API and rejects objects that violate rules you write, using OPA's Rego policy language. This page is production depth, aimed at readers already comfortable with Pods, Services, and Deployments. New to Kubernetes? Start with the Beginner track.
OPA (Open Policy Agent) is a general-purpose policy engine. Gatekeeper is its Kubernetes-native integration - it runs OPA as an admission controller and gives you CRDs to manage policies as Kubernetes objects.
The problem they solve: Kubernetes admission control lets you intercept any API request and decide allow or deny. Without a policy engine, you write and maintain webhooks for each enforcement rule. Gatekeeper centralizes this: write a policy in Rego, deploy it as a CRD, and it's automatically enforced by the admission webhook.
Architecture¶
flowchart TD
User[kubectl apply] --> APIServer[Kubernetes API Server]
APIServer --> |ValidatingWebhook| Gatekeeper[Gatekeeper\nadmission webhook]
Gatekeeper --> |evaluate| OPA[OPA engine]
OPA --> |query| CT[ConstraintTemplates\n+ data cache]
OPA --> |returns| Decision{Allow / Deny}
Decision --> |denied| Reject[Rejected with message]
Decision --> |allowed| APIServer2[Object admitted]
subgraph CRDs
CT2[ConstraintTemplate\ndefines the policy schema]
Con[Constraint\napplies the policy with params]
end
CT2 --> OPA
Con --> OPA
Gatekeeper runs as a Deployment and registers as both a ValidatingAdmissionWebhook and a MutatingAdmissionWebhook. Every API request matching the webhook rules is sent to Gatekeeper for evaluation before being admitted.
OPA evaluates Rego rules against the request object plus any replicated data from the cluster.
ConstraintTemplate and Constraint¶
The two-CRD model separates policy definition from policy application:
- ConstraintTemplate: defines the Rego logic and the schema for parameters
- Constraint: applies a ConstraintTemplate with specific parameters to specific resource scopes
This lets you write a policy once and instantiate it multiple times with different parameters (e.g., require labels, but with different label sets for different namespaces).
ConstraintTemplate example¶
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: requiredlabels
spec:
crd:
spec:
names:
kind: RequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package requiredlabels
violation[{"msg": msg}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("Missing required labels: %v", [missing])
}
A note on Rego syntax versions: every Rego example on this page is written in v0 form (violation[{"msg": msg}] { ... }), which is what Gatekeeper's embedded ConstraintTemplate engine accepts. A standalone OPA 1.0+ binary defaults to v1 syntax and requires the newer form (violation contains {"msg": msg} if { ... }) or the --v0-compatible flag. That matters as soon as you follow the advice below and run opa eval or conftest against these files locally: the same policy that Gatekeeper compiles happily will fail to parse under a current opa.
Constraint applying the template¶
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: RequiredLabels
metadata:
name: require-team-label
spec:
enforcementAction: deny # or "warn" or "dryrun"
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet", "DaemonSet"]
namespaceSelector:
matchExpressions:
- key: environment
operator: In
values: ["production", "staging"]
parameters:
labels:
- team
- cost-center
enforcementAction: deny blocks admission. warn admits but adds a warning to the API response. dryrun only records in audit - never blocks. Use dryrun before deny when rolling out new policies.
Rego language¶
Rego is a declarative language purpose-built for policy. Logic is expressed as rules that evaluate to true/false or sets/objects.
Key concepts¶
package example
# A "violation" rule. Gatekeeper calls this; any element in the set = policy violated.
violation[{"msg": msg}] {
# All expressions in a rule block must be true for the block to produce output.
input.review.object.spec.template.spec.containers[_].securityContext.privileged == true
msg := "Privileged containers are not allowed"
}
# Rules can have multiple blocks -- any block matching = violation
violation[{"msg": msg}] {
container := input.review.object.spec.template.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Container %v has no memory limit", [container.name])
}
input.review.object is the full Kubernetes object being admitted. input.parameters is the Constraint's parameters field.
Common patterns¶
Require image from approved registry:
package approvedregistries
violation[{"msg": msg}] {
container := input.review.object.spec.template.spec.containers[_]
not startswith(container.image, "registry.mycompany.com/")
msg := sprintf("Container %v uses unapproved registry: %v", [container.name, container.image])
}
Block latest tag:
package nolatesttag
violation[{"msg": msg}] {
container := input.review.object.spec.template.spec.containers[_]
endswith(container.image, ":latest")
msg := sprintf("Container %v uses :latest tag", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.template.spec.containers[_]
not contains(container.image, ":")
msg := sprintf("Container %v has no tag (implicitly latest)", [container.name])
}
Require resource limits:
package resourcelimits
violation[{"msg": msg}] {
container := input.review.object.spec.template.spec.containers[_]
not container.resources.limits.cpu
msg := sprintf("Container %v has no CPU limit", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.template.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Container %v has no memory limit", [container.name])
}
Rego gotchas¶
Rego trips up engineers coming from imperative languages because it isn't one - it's a declarative logic language closer to Datalog than to Python or Go. A few things that catch people out repeatedly:
- Everything in a rule body is an implicit AND. Each line is a separate expression, and the rule only produces output if all of them evaluate to true. There's no
&&needed between statements - juxtaposition is conjunction. If you want OR semantics, you write multiple rule bodies with the same head (as theviolationexamples above do) - each body is evaluated independently, and any one succeeding contributes to the result set. someintroduces existential iteration explicitly.container := input.review.object.spec.containers[_]iterates implicitly (the_is a wildcard that ranges over all indices).some i; container := input.review.object.spec.containers[i]does the same thing but binds the index too, which you need when correlating two arrays by position (e.g., matchingcontainers[i]againstsecurityContext.containers[i]overrides in a Pod spec).- Rules don't "return" - they either produce a value or don't exist for that iteration. There's no early exit, no
if/elsein the C sense (Rego'selseexists but attaches to a rule, not a statement). This means you can't "break out" of a loop - instead you filter with conditions and let non-matching iterations simply not contribute. - Undefined is not false.
not input.review.object.spec.foois true both whenfoois explicitly absent and when the whole path doesn't exist - but referencing a deeply nested path that doesn't exist (withoutnot) makes the entire rule body undefined, not false, and it silently produces no result rather than an error. This is the single most common source of "my policy isn't firing" bugs - always test withopa eval -i input.json -d policy.rego "data.pkg.violation"and check whether you getundefinedor[]. - Rego is set/relation-oriented, not procedural. Comprehensions (
{x | ...}) build sets, not lists you loop over with an index. Reach forcount(), set difference (-), andevery(recent OPA versions) before reaching for manual iteration.
A non-trivial constraint template: labels + naming convention¶
Real policies usually combine several conditions. Here's a template that requires specific labels and enforces that metadata.name matches a naming convention (<team>-<env>-<app>) via regex - a common pattern for cost allocation and blast-radius tooling that parses names:
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8snamingconvention
spec:
crd:
spec:
names:
kind: K8sNamingConvention
validation:
openAPIV3Schema:
type: object
properties:
requiredLabels:
type: array
items:
type: string
namePattern:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8snamingconvention
# Violation 1: missing required labels
violation[{"msg": msg}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.requiredLabels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("Missing required labels: %v", [missing])
}
# Violation 2: name doesn't match the required regex
violation[{"msg": msg}] {
pattern := input.parameters.namePattern
name := input.review.object.metadata.name
not regex.match(pattern, name)
msg := sprintf("Object name %q does not match required pattern %q", [name, pattern])
}
# Violation 3: label value must agree with a segment of the name
# (e.g. the "team" label must equal the first dash-delimited segment)
violation[{"msg": msg}] {
some team
team := input.review.object.metadata.labels["team"]
name := input.review.object.metadata.name
segments := split(name, "-")
first_segment := segments[0]
team != first_segment
msg := sprintf("Label team=%q does not match name prefix %q", [team, first_segment])
}
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sNamingConvention
metadata:
name: enforce-naming-convention
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
parameters:
requiredLabels: ["team", "environment"]
namePattern: "^[a-z0-9]+-(prod|staging|dev)-[a-z0-9-]+$"
Note the third violation rule: it cross-checks a label against a piece of the object's own name, which is exactly the kind of correlated validation that's awkward to express with pure JSON-schema-style pattern matching and is where Rego's expressiveness earns its complexity.
Validating what actually gets created: ExpansionTemplate¶
A Constraint matching kind: Pod never sees the Pods created by a Deployment, because the admission request Gatekeeper intercepts is for the Deployment, not the Pods its controller creates downstream. This is a real gap: a policy requiring runAsNonRoot on Pods won't catch a Deployment whose .spec.template.spec violates it, because the object under review is a Deployment, not a Pod.
ExpansionTemplate closes this gap by teaching Gatekeeper how to derive the implicitly-created resource from the parent object, then running all matching Pod-scoped constraints against that derived (synthetic) object before the parent is admitted:
apiVersion: expansion.gatekeeper.sh/v1alpha1
kind: ExpansionTemplate
metadata:
name: expand-deployments
spec:
applyTo:
- groups: ["apps"]
kinds: ["Deployment"]
versions: ["v1"]
templateSource: "spec.template"
enforcementAction: deny
generatedGVK:
kind: "Pod"
group: ""
version: "v1"
With this in place, a Constraint written against kind: Pod (e.g. the resource-limits or naming policies above) is automatically evaluated against the synthesized Pod spec extracted from every Deployment, StatefulSet, DaemonSet, Job, or CronJob that Gatekeeper is configured to expand - you don't need to duplicate the policy logic for each wrapper kind. Gatekeeper ships built-in expansions for the common workload controllers; you generally only write your own ExpansionTemplate for custom resources with their own Pod-template-shaped fields (e.g. a CRD-based operator).
Audit¶
Gatekeeper's audit controller periodically evaluates all existing objects against active Constraints, even objects that were admitted before the policy existed. Violations are recorded in the Constraint's status.violations field:
kubectl describe requiredlabels require-team-label
# Status:
# Audit Timestamp: 2026-05-10T20:00:00Z
# Violations:
# - Enforcement Action: deny
# Kind: Deployment
# Message: Missing required labels: {"cost-center"}
# Name: legacy-app
# Namespace: production
Audit runs every 60 seconds by default (--audit-interval). Use it to assess the blast radius of a new policy before switching from dryrun to deny.
Data replication¶
Rego can reference cluster data beyond the admission request - for example, checking if a Service with the same name already exists, or comparing against a list of approved namespaces. Gatekeeper replicates this data into OPA via the Config CRD:
apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata:
name: config
namespace: gatekeeper-system
spec:
sync:
syncOnly:
- group: ""
version: v1
kind: Namespace
- group: ""
version: v1
kind: Pod
- group: "apps"
version: v1
kind: Deployment
Replicated data is available in Rego as data.inventory.cluster[kind][name] or data.inventory.namespace[namespace][kind][name]. Use sparingly - replication increases Gatekeeper's memory footprint.
Using replicated data in a policy¶
A common use case: block a new Ingress if its hostname collides with an Ingress that already exists in a different namespace. This is impossible to check by looking at the incoming object alone - you need to see the rest of the cluster's Ingresses:
Gatekeeper exposes replicated objects under a four-level path: data.inventory.namespace[<namespace>][<apiVersion>][<kind>][<name>] (cluster-scoped kinds live under data.inventory.cluster[<apiVersion>][<kind>][<name>] instead). Note that the second slot is the apiVersion, not the kind - getting those two backwards is the single most common mistake in inventory-based policies.
package uniqueingresshost
violation[{"msg": msg}] {
new_host := input.review.object.spec.rules[_].host
some ns, name
existing := data.inventory.namespace[ns]["networking.k8s.io/v1"]["Ingress"][name]
existing_host := existing.spec.rules[_].host
existing_host == new_host
ns != input.review.object.metadata.namespace
msg := sprintf("Hostname %q is already used by Ingress %v/%v", [new_host, ns, name])
}
This only works because the Config resource above lists Ingress (or, generically, whatever GVKs you sync) under syncOnly. Every additional kind you replicate is another watch Gatekeeper maintains and another chunk of memory it holds in the OPA in-memory store - in clusters with tens of thousands of objects, be deliberate about what you sync, and prefer narrow, specific GVKs over syncing broad categories "just in case."
Mutation¶
Gatekeeper's mutation feature modifies objects at admission time (like a MutatingAdmissionWebhook). Use it to inject defaults, labels, or security context settings:
apiVersion: mutations.gatekeeper.sh/v1
kind: Assign
metadata:
name: set-default-security-context
spec:
applyTo:
- groups: ["apps"]
kinds: ["Deployment"]
versions: ["v1"]
match:
scope: Namespaced
namespaces: ["production"]
location: "spec.template.spec.containers[name: *].securityContext.readOnlyRootFilesystem"
parameters:
assign:
value: true
Mutation runs before validation. A common pattern: mutate to add required labels/annotations, then validate that they exist. This avoids breaking users who don't know the policy yet.
The mutation CRDs: Assign, AssignMetadata, ModifySet¶
Gatekeeper splits mutation into three purpose-built CRDs rather than one generic patch mechanism, because metadata, arbitrary fields, and list membership all have different merge semantics:
Assignsets or replaces a value at a specific path - used above to forcereadOnlyRootFilesystem: true. Works on any field, including deep into container specs.AssignMetadataisAssignrestricted tometadata.labelsandmetadata.annotations. It exists as a separate CRD because label/annotation mutation has narrower, safer semantics (it can't touchspec) and Gatekeeper enforces that boundary at the API level, not just by convention.ModifySetadds or removes individual elements from a list-valued field, like a container'sargsor a Pod'simagePullSecrets, without clobbering the rest of the list - somethingAssigncan't do cleanly against an array.
apiVersion: mutations.gatekeeper.sh/v1
kind: AssignMetadata
metadata:
name: add-managed-by-label
spec:
match:
scope: Namespaced
location: "metadata.labels.managed-by"
parameters:
assign:
value: "gatekeeper"
apiVersion: mutations.gatekeeper.sh/v1
kind: ModifySet
metadata:
name: add-image-pull-secret
spec:
match:
scope: Namespaced
kinds:
- apiGroups: [""]
kinds: ["Pod"]
location: "spec.imagePullSecrets"
parameters:
operation: merge
values:
fromList:
- name: registry-mycompany-com
Mutation ordering and conflicts¶
Multiple mutation policies can apply to the same object. Gatekeeper resolves this deterministically:
- Mutations are applied in a defined order based on
locationpath specificity and mutation name (not creation timestamp) - so ordering is stable across restarts and reconciles, but it is not something you control by editing YAML order. - Two mutations that write to the same location with different values is a genuine conflict. Gatekeeper detects this at admission time and rejects the request rather than silently picking a winner - you'll see an error referencing "conflicting mutations" in the API response. This is a deliberate fail-closed choice: silently applying one of two contradictory security defaults would be worse than blocking the request.
- Mutation is re-run to a fixpoint: because a mutation can itself change a field another mutation depends on (e.g. one
Assignadds a label, anotherAssignreads that label via a templated location), Gatekeeper iterates mutations up to a bounded number of passes until the object stops changing. Keep mutation chains shallow - if you find yourself relying on a third pass to converge, the policies are too entangled and are worth consolidating into one.
In practice: keep mutation policies narrowly scoped (one concern per policy), avoid two policies touching the same field, and test mutation output with kubectl apply --dry-run=server before rolling out broadly, since dry-run still goes through the mutating webhook.
Exemptions¶
Some resources (Gatekeeper itself, system namespaces) must be exempt from policy enforcement:
Or globally via the Config CRD:
Performance and scaling in large clusters¶
Gatekeeper sits on the API server's request path for every matched object - a slow or overloaded Gatekeeper directly slows down or blocks cluster operations. In clusters with high object churn (large CI/CD platforms, multi-tenant clusters with hundreds of namespaces), a handful of tuning knobs matter:
- Webhook timeout.
failurePolicyandtimeoutSecondson theValidatingWebhookConfigurationdetermine what happens if Gatekeeper doesn't answer in time. Kubernetes caps webhook timeouts at 30 seconds; Gatekeeper's own default is much lower (a few seconds). If you have Constraints doing expensive data-replication lookups, raise the timeout deliberately rather than letting requests fail - but every second added here is a second every matchedkubectl applycan stall for. PreferfailurePolicy: Failin production (fail closed) but only after you're confident in Gatekeeper's own availability - pair it with a PodDisruptionBudget and at least 2 replicas, sinceFailmeans a Gatekeeper outage blocks all matched admission. --audit-chunk-size. The audit controller lists every object of every watched kind and evaluates it against every Constraint. In clusters with tens of thousands of objects, listing everything in one API call is expensive and slow to page through.--audit-chunk-size(set on the audit Deployment/controller-manager args) paginates the list calls, trading a longer audit cycle for lower peak memory and API server load. Increase--audit-intervalalongside it if a full audit pass starts taking longer than the interval.- Constraint and template count. Every additional Constraint is another Rego evaluation per matched request. Rego evaluation is fast in absolute terms, but it's not free at thousands of requests per minute - consolidate related checks into fewer, richer ConstraintTemplates (as in the naming-convention example above) rather than one ConstraintTemplate per tiny rule, both for evaluation overhead and for readability of the audit output.
- Data replication cache size. As covered above,
Config-driven sync holds replicated objects in memory inside the Gatekeeper pod. Watch Gatekeeper's own memory usage (kubectl top pod -n gatekeeper-system) after adding newsyncOnlyentries - this is the most common cause of Gatekeeper pods getting OOMKilled in large clusters. - Replica count and resource requests. Gatekeeper's webhook pods should run with multiple replicas behind the Service that the
ValidatingWebhookConfigurationpoints to, both for availability and to spread admission-request load. Give the pods real CPU requests - Rego evaluation is CPU-bound, and a throttled Gatekeeper pod is indistinguishable from a slow one from the API server's perspective. matchnarrowing. The single highest-leverage performance change is usually the most obvious one: scopematch.kindsandmatch.namespaceSelectoron every Constraint as tightly as possible. A Constraint matchingkinds: ["*"]across all namespaces gets evaluated on every single admission request cluster-wide, even ones it will never violate.
Gatekeeper and Kubernetes ValidatingAdmissionPolicy¶
Kubernetes' own admission control has grown a native CEL-based mechanism - ValidatingAdmissionPolicy (GA since Kubernetes v1.30) and, more recently, MutatingAdmissionPolicy (GA since Kubernetes v1.36) - that runs inside the API server itself, with no webhook round-trip. Gatekeeper doesn't treat this as a competitor; it integrates with it directly:
- CEL constraint evaluation. Since Gatekeeper v3.18, a ConstraintTemplate can express its logic in CEL instead of (or alongside) Rego, evaluated via the
K8sNativeValidationengine. - Generating
ValidatingAdmissionPolicyobjects. Since v3.20 (beta, enabled by default), Gatekeeper can automatically generate a matchingValidatingAdmissionPolicyandValidatingAdmissionPolicyBindingfrom a ConstraintTemplate/Constraint pair - letting eligible policies run in-process in the API server instead of round-tripping through Gatekeeper's webhook, while still being authored and managed the same way (gator test, audit, theConstraintCRD). This is controlled by the--default-create-vap-for-templatesand--default-create-vap-binding-for-constraintsflags, and per-policy viagenerateVAPon the template.
The practical upshot: if a policy's logic is expressible in CEL and doesn't need Rego's data-replication or cross-object correlation features, Gatekeeper can now offload it to native Kubernetes admission control for lower latency, while everything more complex still goes through the OPA-evaluated path described in the rest of this page.
Testing with gator and conftest¶
Two tools cover Gatekeeper policy testing before anything reaches a live admission webhook.
gator test is Gatekeeper's own CLI (part of the gator binary) and understands ConstraintTemplates and Constraints natively - no translation needed:
# Structure: templates/*.yaml, constraints/*.yaml, test-inputs/*.yaml (objects to test against)
gator test --filename=templates/k8snamingconvention.yaml \
--filename=constraints/enforce-naming-convention.yaml \
--filename=test-inputs/
gator test reports pass/fail per input object against the loaded Constraints, using the exact same evaluation path Gatekeeper's webhook uses - so a policy that passes gator test will behave identically in the cluster, unlike hand-rolled Rego unit tests that only exercise the violation rule in isolation. Run gator test in CI on every pull request that touches templates/ or constraints/, before the ConstraintTemplate is ever synced to the cluster.
gator has grown beyond test into a small suite: gator verify runs structured test suites (Suite/Test/Case objects, including AdmissionReview-based cases and ExpansionTemplate validation), gator expand lets you inspect what a workload resource expands to without applying anything, and gator bench measures Rego/CEL policy evaluation latency and throughput - useful when the performance tuning in the previous section needs real numbers instead of guesses.
conftest lets you unit-test Rego policies against YAML fixtures directly, independent of the ConstraintTemplate/Constraint wrapping - useful when you're iterating on the Rego logic itself and don't want to round-trip through the CRD schema on every change:
# test/required_labels_test.rego
package requiredlabels
test_missing_label_violation {
violations := violation with input as {
"review": {"object": {
"metadata": {"labels": {"team": "platform"}},
"spec": {}
}},
"parameters": {"labels": ["team", "cost-center"]}
}
count(violations) == 1
}
test_all_labels_present {
violations := violation with input as {
"review": {"object": {
"metadata": {"labels": {"team": "platform", "cost-center": "eng-infra"}},
"spec": {}
}},
"parameters": {"labels": ["team", "cost-center"]}
}
count(violations) == 0
}
OPA without Gatekeeper¶
Gatekeeper is the right choice for Kubernetes admission control, but it's worth understanding that Gatekeeper is one integration among many of a general-purpose policy engine. OPA itself has no idea what a Pod is - it evaluates Rego against arbitrary JSON input and returns a decision, and Gatekeeper is just the piece of glue that shapes Kubernetes admission requests into that JSON and wires the result back into a webhook response. The same skill investment in Rego pays off well outside Kubernetes:
- CI/CD policy gates.
conftest(used above for testing) is really OPA pointed at Terraform plans, Dockerfiles, Kubernetes manifests, or any structured config, run as a CI step that fails the pipeline before a bad change ever reaches a cluster. Many teams run the same Rego policies throughconftestin CI and through Gatekeeper at admission time - CI catches the mistake early with a fast feedback loop, and Gatekeeper is the backstop for anything that bypasses CI (akubectl applyfrom a laptop, a controller reconciling a CR). - API authorization. OPA runs as a sidecar or library next to application services to answer "can this user perform this action on this resource?" - the same role RBAC plays inside Kubernetes, but for your own APIs. This is the pattern behind projects like Styra's Enterprise OPA and is common in service-mesh-adjacent authorization (envoy's external authorization filter can call out to OPA).
- Infrastructure and data access control. OPA evaluates policy for Terraform Cloud/Enterprise runs, Boundary access requests, and Kafka topic ACLs, among others - anywhere a system needs "evaluate this request against a policy and return allow/deny" as a discrete, auditable step.
If your organization already runs OPA for one of these, standing up Gatekeeper is mostly plumbing - the Rego, the mental model of input/data, and the testing workflow all transfer directly. Conversely, if Gatekeeper is your first exposure to OPA, the Rego you learn here is directly reusable the moment you need policy enforcement somewhere that isn't Kubernetes.
Common mistakes¶
- Writing
denyas the enforcement action from day one. Every new Constraint should start indryrun(orwarn), run through at least one full audit cycle, and get reviewed for false positives before flipping todeny. Skipping this step is the single most common cause of a Friday-afternoon outage caused by policy rollout. - Forgetting
excludedNamespacesforkube-systemand Gatekeeper's own namespace. A misconfigured Constraint that matches cluster-wide can block Gatekeeper's own pods from updating, or block core controllers, effectively locking the cluster. - Treating
noton a possibly-undefined deep path as equivalent to a boolean false check. As covered above, referencing an undefined path withoutnotmakes the whole rule body undefined rather than false - test every rule against both "field present with wrong value" and "field entirely absent" fixtures. - Syncing too much cluster state via
Config. It's tempting to syncPod,Deployment,Service, and everything else "in case a policy needs it later." Each synced GVK is ongoing memory and watch overhead whether or not any Constraint actually reads it. Sync only what current policies use. - One giant ConstraintTemplate that tries to do everything, or dozens of one-line ones. Neither extreme is maintainable - group logically related checks (as in the naming-convention example) into one template, and keep templates focused on one policy domain (naming, labels, image provenance, resource governance) rather than one Rego file per organization.
- No CI testing before merge. ConstraintTemplates and Constraints are YAML like anything else in your GitOps repo - run
gator testin CI on every change, the same way you'd run unit tests on application code. A syntax error in Rego that only surfaces when Gatekeeper tries to compile the template at apply time is a bad way to find out. - Ignoring mutation/validation ordering. If a validation Constraint requires a field that a mutation policy is supposed to inject, and the mutation webhook is misconfigured or excluded for that namespace, the validation will fail in a way that looks like a policy bug but is actually a mutation coverage gap. When debugging a validation failure, check whether mutation ran first.
Source Links¶
- Open Policy Agent documentation
- Rego policy language reference
- OPA 1.0 announcement (v0 vs v1 Rego syntax)
- Gatekeeper documentation
- Gatekeeper: replicating data (
Configanddata.inventory) - Gatekeeper: mutation
- Gatekeeper:
gatorCLI for policy testing - Gatekeeper policy library
- open-policy-agent/gatekeeper on GitHub
- CNCF project page: Open Policy Agent
- Kubernetes: dynamic admission control
Related Concepts¶
- Kyverno - the YAML-native alternative; see its page for a full head-to-head comparison table
- Falco - runtime detection layer that complements Gatekeeper's admission-time enforcement
- RBAC
- Pod Security