Safeguard
DevSecOps

A hands-on introduction to Rego for Kubernetes admission control

OPA graduated CNCF on January 29, 2021, and Rego v1 became the default syntax in OPA v1.0.0 (Dec 2024) — here's how to write your first admission-control policies.

Safeguard Research Team
Research
6 min read

Open Policy Agent (OPA) was donated to the Cloud Native Computing Foundation on March 29, 2018, and reached CNCF's highest maturity tier — graduated status — on January 29, 2021, putting it in the same tier as Kubernetes and Prometheus. Its policy language, Rego (pronounced "ray-go"), is not a general-purpose scripting language; it's a declarative query language purpose-built to evaluate rules over JSON-like structured data, which is exactly what a Kubernetes AdmissionReview request is. That makes OPA a natural fit for admission control: intercept a kubectl apply, hand the resulting JSON to a Rego policy, and get back an allow/deny decision before the object ever hits etcd. The ecosystem shifted meaningfully in December 2024, when OPA v1.0.0 made "Rego v1" syntax the default — requiring explicit if keywords in rule bodies and contains for partial set rules, so policies no longer need import future.keywords to use syntax that had been opt-in for years. This post walks through the actual building blocks — packages, rules, values, and boolean composition — using real pod-security examples you could drop into a cluster today, whether you enforce them through a hand-rolled validating webhook or through OPA Gatekeeper, the dedicated CNCF admission controller built on top of OPA.

What does a minimal Rego policy actually look like?

Every Rego file starts with a package declaration, which namespaces its rules so OPA can address them via a query path. A minimal admission policy looks like this: package kubernetes.admission followed by a rule such as default allow := false and then allow if { input.request.kind.kind == "Pod" }. The input document is whatever JSON you hand OPA at evaluation time — for Kubernetes admission control, that's the AdmissionReview object the API server sends, so input.request.object.spec gets you straight to the PodSpec being created. Under Rego v1 (the default since OPA v1.0.0 in December 2024), the if keyword is mandatory in rule bodies like the one above; pre-v1.0 policies without import future.keywords.if would need to either add that import or keep writing the older implicit-body syntax. You test this locally with opa eval -i input.json -d policy.rego "data.kubernetes.admission.allow", or paste it straight into the Rego Playground at play.openpolicyagent.org — no cluster required.

How do rules and values work in Rego?

A Rego rule is a logical implication: the rule's head is true (or takes on a given value) exactly when its body evaluates to true. The simplest form, a "complete rule," assigns a single value — allow := true if { input.request.operation == "CREATE" } says allow is true only when that condition holds; if no rule body matches, allow is simply undefined, which is why policies almost always pair this with a default allow := false line above it. Partial rules build up a set or object instead of a single scalar, using contains under Rego v1 syntax: violation contains msg if { input.request.object.spec.containers[_].image == "latest"; msg := "image tag 'latest' is not allowed" } adds a string to the violation set every time an image without a fixed tag is found. Multiple rule bodies sharing the same head are OR'd together — any one of them being true is enough — except for complete rules, where two bodies producing genuinely conflicting values (not just duplicate identical ones) cause OPA to raise an evaluation error rather than silently pick a winner.

How do you express AND versus OR logic in a policy body?

Inside a single rule body, every statement is implicitly ANDed together — a body is only true if every line in it evaluates to true, evaluated top to bottom with Rego's usual unification semantics. A rule requiring both a resource limit and a non-root user might read: allow if { input.request.object.spec.containers[_].resources.limits.memory; input.request.object.spec.securityContext.runAsNonRoot == true } — both conditions must hold for that single rule body to succeed. OR logic, by contrast, comes from writing multiple separate rule bodies (or separate rules) with the same head, since Rego evaluates each independently and unions the results: two allow if { ... } blocks, one for Pod and one for Deployment kinds, together produce "allow if either condition is met." This is a common trip-up for developers coming from imperative languages — there's no && or || operator inside a body; comma-separated (or newline-separated) expressions are the AND, and additional rule bodies are the OR.

How does OPA plug into a live Kubernetes admission chain?

There are two well-established patterns. The first is standalone OPA wired directly into Kubernetes as a validating webhook: the API server calls out with an AdmissionReview, OPA evaluates it against a allow/deny policy loaded from a ConfigMap or bundle, and returns an AdmissionReview response with allowed: true/false — this is the classic teaching setup for denying pods that run as root, block resource limits, or reference latest-tagged images. The second, more common in production, is OPA Gatekeeper — a separate CNCF project layered on OPA that packages policies as Kubernetes-native ConstraintTemplate and Constraint custom resources instead of raw ConfigMaps, giving policies proper CRD status, versioning, and kubectl get constraints visibility. Gatekeeper is actively maintained, with releases continuing through its 3.x line into 2026, and remains the recommended path for teams that want policy violations to show up as native Kubernetes objects rather than webhook logs.

What's the practical workflow for iterating on a policy before it hits a cluster?

The standard loop is: write the .rego file, run opa eval against a saved input.json sample to check the decision, write unit tests with opa test (Rego supports test_ prefixed rules that assert expected outcomes against fixture inputs), and only then bundle the policy for the webhook or Gatekeeper ConstraintTemplate. opa test ./policies -v runs every test file in a directory and reports pass/fail per assertion, which is what lets a policy repository run in CI the same way application unit tests do — catching a typo in a field path (input.request.object.spc.containers instead of spec) before it either silently allows everything or blocks every deployment in the cluster. The Rego Playground offers the same evaluate-and-inspect loop in a browser, including a coverage view that highlights which lines of a policy a given input actually exercised, which is useful for confirming a deny rule's condition is reachable at all before trusting it in front of production traffic.

How Safeguard thinks about policy-as-code enforcement points

Kubernetes admission control is one instance of a broader pattern Safeguard cares about across the software supply chain: putting a machine-checkable policy at every point where an artifact could enter your environment, rather than relying on a human to remember a checklist. Whether that enforcement point is a Rego-based admission webhook deciding whether a pod can start, or a build-time gate deciding whether an SBOM shows an unpatched CVE, the underlying discipline is the same — express the rule declaratively, test it against real inputs before it's live, and fail closed by default. Safeguard's own guardrails and policy gates apply that same enforcement-point model to build and deployment pipelines, giving security teams a queryable, auditable record of every policy decision alongside the vulnerability and SBOM data that decision was based on.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.