Back to Distributed Systems & Kubernetes Series

OPA/Gatekeeper Track Part 1: Rego Basics

June 6, 2026 Wasil Zafar 38 min read

Open Policy Agent (OPA) is a general-purpose policy engine. Its language, Rego, evaluates structured data (JSON) against rules. For Kubernetes, OPA acts as an admission webhook — every API server request is evaluated by your Rego policies before the resource is created. If the policy denies, the request is rejected with your custom message.

Table of Contents

  1. OPA Concepts
  2. Rego Language Basics
  3. Functions & Built-ins
  4. Kubernetes Admission Context
  5. Testing with OPA CLI
  6. Exercises
  7. Key Takeaways & Next Steps

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 .rego files.
  • 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) or deny (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

Exercise 1 — Rego REPL: Install OPA CLI. Write a policy that denies Pods from the 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).
Exercise 2 — Comprehensions: Write a policy that collects all container names with no liveness probe into an array, then denies with a message listing all the offending containers. Write a unit test for it.
Exercise 3 — Exemptions: Add an exemption mechanism to your policy: if the namespace is in an exempt_namespaces set (loaded from data), skip the policy. Write tests for both the exempt and non-exempt cases.

Key Takeaways & Next Steps

Key Takeaways:
  • 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 test command catches policy regressions
  • The input document 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.