The general-purpose policy engine — decouple authorization and policy decisions from your services using Rego, and enforce them across Kubernetes, APIs, CI, and Terraform. You'll learn Rego, evaluate policies with the CLI, guard Kubernetes with Gatekeeper, and test configs with conftest. Hands-on throughout.
The journey:
| You need | Why |
|---|---|
The opa CLI |
Evaluate and test policies locally |
| A terminal | Everything starts on the CLI |
| (Optional) a Kubernetes cluster | For the Gatekeeper section |
Install from openpolicyagent.org (or brew install opa). Rego takes practice — start with the CLI's fast feedback loop before touching a cluster. Confirm: opa version.
OPA is a general-purpose policy engine that answers one question: given this input, is it allowed (or what should happen)? Your service or platform sends OPA a JSON input; OPA evaluates policy (written in Rego) against optional data and returns a decision (allow/deny, a set of violations, a filtered result).
The value is decoupling: instead of authorization logic scattered through code, policy lives in one place, versioned and testable. OPA is domain-agnostic — the same engine guards Kubernetes admission, microservice API authz, Terraform plans, CI pipelines, and Envoy/Kafka. Kubernetes-specific policy usually uses Gatekeeper, which wraps OPA as an admission controller.
A policy is a .rego file. Save example.rego:
package example
# Deny if the input user is not an admin
default allow := false
allow if {
input.user.role == "admin"
}
Evaluate it against some input:
opa eval -d example.rego -i input.json 'data.example.allow'
# Or interactively:
echo '{"user": {"role": "admin"}}' | opa eval -d example.rego -I 'data.example.allow'
data.example.allow is the decision path (data + package + rule). OPA returns true/false (or richer results). The REPL (opa run) is great for experimenting.
Rego is a declarative query language. The core ideas:
package k8s
# A rule is true if ALL its conditions hold (logical AND)
is_privileged if {
input.spec.containers[_].securityContext.privileged == true
}
# `_` iterates a collection; the rule is true if ANY element matches (existential)
# Build a SET of violation messages
deny contains msg if {
some c in input.spec.containers
c.image == "nginx:latest"
msg := sprintf("container %s uses :latest", [c.name])
}
Key concepts: rules produce values (booleans, sets, objects); a rule body is an implicit AND of its lines; [_] or some x in iterates; unmatched conditions make a rule undefined (not an error). Policies typically build a deny set of messages — empty set means allowed.
OPA evaluates against two documents:
input — the thing being decided (an admission review, an HTTP request, a Terraform plan).data — external context you load (allowed registries, role mappings, org rules).package authz
import data.roles # loaded from a JSON/YAML data file
allow if {
input.method == "GET"
input.user in data.roles.readers
}
You supply data from files (-d data.json) or a bundle server, so policy logic stays constant while the reference data changes. The decision is whatever path you query — a boolean, the deny set, or a transformed object.
Gatekeeper runs OPA as a Kubernetes admission controller and packages Rego into two CRDs: a ConstraintTemplate (the reusable Rego logic) and Constraints (instances that apply it with parameters).
# ConstraintTemplate — the reusable rule
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}] {
required := input.parameters.labels
provided := input.review.object.metadata.labels
missing := required[_]
not provided[missing]
msg := sprintf("missing required label: %v", [missing])
}
---
# Constraint — apply it to Namespaces, requiring an "owner" label
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata: { name: ns-must-have-owner }
spec:
match: { kinds: [{ apiGroups: [""], kinds: ["Namespace"] }] }
parameters: { labels: ["owner"] }
Gatekeeper supports enforcementAction: dryrun|warn|deny — the same audit-then-enforce discipline as Kyverno. It also audits existing resources against constraints, not just new ones.
conftest uses Rego to test any structured config — Dockerfiles, Terraform plans, Kubernetes YAML, CI files — in your pipeline:
# policy/deploy.rego
package main
deny contains msg if {
input.kind == "Deployment"
not input.spec.template.spec.securityContext.runAsNonRoot
msg := "Deployments must run as non-root"
}
conftest test deployment.yaml # fails the build on a deny
conftest test main.tf --parser hcl2 # yes, Terraform too
This brings OPA's power to CI without a cluster — gate merges on the same policy language you use at admission. It's how teams enforce standards on IaC and manifests before they ship.
Rego has a built-in test framework — policies are code, so test them:
# example_test.rego
package example
test_admin_allowed if {
allow with input as {"user": {"role": "admin"}}
}
test_user_denied if {
not allow with input as {"user": {"role": "viewer"}}
}
opa test . # runs every test_ rule
opa fmt -w . # canonical formatting
with input as {...} injects test input. Cover the allow and deny paths, edge cases, and any data dependencies. Run opa test in CI so a policy change can't silently break enforcement.
Both do Kubernetes admission policy; they differ in language and scope:
Rule of thumb: Kyverno when your policy needs are Kubernetes-centric and you want YAML; OPA when you need one policy language across many systems (service authz + IaC + admission) or complex logic Rego expresses better. Many orgs run both.
opa fmt, and opa test in CI — policies are code.deny/violation set of clear messages — tell users exactly what's wrong.deny.data) from logic so it changes without editing rules.failurePolicy deliberately.default values and negation deliberately.[_] without understanding existential vs universal semantics — a frequent Rego bug.deny before dryrun/audit. Same lesson as every admission engine: preview first.data, not hardcoded in rules.opa test is cheap.failurePolicy: Fail blocks all admissions.allow in Rego, evaluate it with opa eval against two inputs (admin and viewer).deny that collects messages for pods using :latest; test it on a sample pod JSON.owner label on Namespaces; create one without it in dryrun, then deny.conftest test on a good and bad manifest.test_ rules with with input as and run opa test ..Self-check:
input, data, and a "decision" in OPA?You now have the loop: Rego → input/data/decision → Gatekeeper → conftest → test → choose the right engine. That's OPA as a policy decision service across your stack.