OPA Concepts
OPA decouples policy decisions from policy enforcement. Your application (or Kubernetes) sends a JSON query to OPA. OPA evaluates it against loaded policies and data, returning a decision. The application enforces that decision.
- Policy — Rego code that defines rules. Stored in
.regofiles. - Data — JSON documents that policies can reference (e.g., allowlists, configuration).
- Query — A Rego expression evaluated at runtime. For K8s admission: the AdmissionReview JSON.
- Decision — The result of evaluating the query. For K8s:
allow(boolean) ordeny(set of messages).
Rego Language Basics
Rules & Expressions
# Rego uses a declarative style — you define what must be true
# Rules evaluate to true or false (or a value)
package kubernetes.admission # Package declaration — namespace for rules
# Complete rule: deny is a set that gets populated when conditions are met
deny[msg] {
# Input refers to the data being evaluated (the K8s admission request)
input.request.kind.kind == "Deployment"
# Access nested fields with dot notation
container := input.request.object.spec.template.spec.containers[_]
# Condition: container has no resource limits
not container.resources.limits
# Build the error message
msg := sprintf("Container %v in Deployment %v has no resource limits",
[container.name, input.request.object.metadata.name])
}
# The rule 'deny' is a set — multiple rule heads with the same name
# collect all matching messages into a single set
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
container.securityContext.privileged == true
msg := sprintf("Container %v must not run as privileged", [container.name])
}
Data Model
# In Rego, everything is accessed via dot notation or bracket notation
# 'input' is the query document (e.g., K8s AdmissionReview)
# 'data' is the loaded policy data (allowlists, configs)
# Example: accessing nested arrays
# container := input.request.object.spec.containers[_]
# The [_] is a wildcard — iterates over all elements
# Example: checking a specific key exists
"allowed-registries" in input.request.object.metadata.annotations
# Example: checking a value in a set
allowed_registries := {"gcr.io/my-project", "ghcr.io/my-org"}
startswith(image, registry) {
registry := allowed_registries[_]
}
Comprehensions
# Array comprehension — like a list comprehension in Python
all_container_images := [img |
container := input.request.object.spec.containers[_]
img := container.image
]
# Set comprehension — deduplicated
unique_namespaces := {ns |
pod := data.pods[_]
ns := pod.metadata.namespace
}
# Object comprehension
image_by_container := {name: image |
container := input.request.object.spec.containers[_]
name := container.name
image := container.image
}
# Use comprehensions to check if any/all conditions hold
# 'any container has no limits' pattern
containers_without_limits := [c.name |
c := input.request.object.spec.containers[_]
not c.resources.limits
]
deny[msg] {
count(containers_without_limits) > 0
msg := sprintf("These containers have no limits: %v", [containers_without_limits])
}
Functions & Built-ins
# Define reusable functions
is_exempt_namespace(ns) {
exempt_namespaces := {"kube-system", "kube-public", "vault"}
ns == exempt_namespaces[_]
}
# Use the function in rules
deny[msg] {
ns := input.request.namespace
not is_exempt_namespace(ns)
container := input.request.object.spec.containers[_]
# ... policy check
msg := "..."
}
# Commonly used Rego built-ins
count(collection) # Count array/set/object elements
startswith(str, prefix) # String prefix check
endswith(str, suffix) # String suffix check
contains(str, substr) # String contains
concat(delim, arr) # Join array elements
split(str, delim) # Split string into array
sprintf(fmt, args) # Format string
regex.match(pattern, str) # Regex match
is_null(x) # Null check
object.get(obj, key, default) # Safe object access with default
any(collection) # True if any element is true
all(collection) # True if all elements are true
min(set) # Minimum value
max(set) # Maximum value
Kubernetes Admission Context
# When OPA acts as an admission webhook, 'input' is the AdmissionReview object
# Key fields:
input.request.uid # Request ID
input.request.kind.group # API group
input.request.kind.version # API version
input.request.kind.kind # Resource kind (Pod, Deployment, etc.)
input.request.namespace # Target namespace
input.request.operation # CREATE, UPDATE, DELETE, CONNECT
input.request.object # The resource being created/modified
input.request.oldObject # Previous state (UPDATE only)
input.request.userInfo.username # Who is making the request
input.request.userInfo.groups # Group memberships
# Example: a complete policy that enforces required labels
package kubernetes.admission
deny[msg] {
# Only apply to Deployment creates/updates
input.request.kind.kind == "Deployment"
input.request.operation == "CREATE"
# Check required labels
required_labels := {"app", "team", "version"}
existing_labels := {l | input.request.object.metadata.labels[l]}
missing := required_labels - existing_labels
count(missing) > 0
msg := sprintf("Deployment is missing required labels: %v", [missing])
}
Testing with OPA CLI
# Install OPA CLI
brew install opa
# or
curl -L -o opa https://openpolicyagent.org/downloads/v0.68.0/opa_linux_amd64_static
chmod +x opa
# Check a policy file for syntax errors
opa check policy.rego
# Evaluate a policy against an input file
opa eval \
--data policy.rego \
--input admission-review.json \
"data.kubernetes.admission.deny"
# Run OPA in REPL mode for interactive policy testing
opa run policy.rego
# > data.kubernetes.admission.deny with input as {"request": {...}}
# Write unit tests in Rego
# test_policy.rego
package kubernetes.admission_test
import data.kubernetes.admission
test_deny_privileged_container {
input := {
"request": {
"kind": {"kind": "Pod"},
"object": {
"spec": {
"containers": [{
"name": "app",
"securityContext": {"privileged": true}
}]
}
}
}
}
count(admission.deny) > 0 with input as input
}
test_allow_non_privileged_container {
input := {
"request": {
"kind": {"kind": "Pod"},
"object": {
"spec": {
"containers": [{
"name": "app",
"securityContext": {"privileged": false}
}]
}
}
}
}
count(admission.deny) == 0 with input as input
}
# Run unit tests
opa test policy.rego test_policy.rego -v
# PASS: test_deny_privileged_container (234µs)
# PASS: test_allow_non_privileged_container (98µs)
# ----------------------------------------
# PASS: 2/2
Exercises
latest image tag. Test it against two JSON inputs: one with image: nginx:latest (should deny) and one with image: nginx:1.25 (should allow).
exempt_namespaces set (loaded from data), skip the policy. Write tests for both the exempt and non-exempt cases.
Key Takeaways & Next Steps
- Rego is declarative — you define what must be true, not how to evaluate it step by step
- The
[_]wildcard iterates over all elements of an array;:=assigns variables - Rules with the same name form a set — multiple deny messages collect into a single response
- Comprehensions let you build arrays/sets/objects from collections
- Always write Rego unit tests — the
opa testcommand catches policy regressions - The
inputdocument is the K8s AdmissionReview JSON — your policy shapes match it
Next in This Track
In Part 2: Gatekeeper & Constraints, we install OPA Gatekeeper (the Kubernetes-native OPA distribution), define ConstraintTemplates with your Rego code, and create Constraint instances that enforce policies on specific resource types and namespaces.