OPA and Gatekeeper¶
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])
}
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])
}
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.
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.
Exemptions¶
Some resources (Gatekeeper itself, system namespaces) must be exempt from policy enforcement:
Or globally via the Config CRD:
Testing with conftest¶
Conftest lets you unit-test Rego policies against YAML fixtures locally, before deploying to Gatekeeper:
# 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 OPA itself is useful beyond admission - policy checks in CI pipelines (conftest), API gateway authorization, data pipeline access control. The same Rego you write for Gatekeeper can be used in these contexts with opa eval or the OPA bundle server.