Rego Development Best Practices
Core Conventions
- OPA v1.0+ uses Rego v1 semantics by default (
if/containskeywords required, duplicate imports prohibited). Useimport rego.v1for compatibility across OPA versions — it's a no-op in v1.0+ and opts in on older versions. - Use
--v0-compatibleflag when migrating legacy policies to v1.0. - Always use
opa fmtfor consistent formatting. Useopa fmt --rego-v1to auto-rewrite policies to v1 syntax. - Use
opa checkin build pipelines (strict mode is default in v1.0). - Prioritize clear code over assumed performance optimizations — OPA handles most optimization automatically.
Naming & Style
snake_casefor all rule names and variables (user_is_admin).- Leading underscore for internal helpers (
_is_developer(user)). - Use descriptive rule names:
usersnotget_users. Exception:is_/has_for booleans. - Break down complex logic into well-named helper rules.
Rule Design
Handle undefined values
The most common Rego bug — undefined intermediates silently bypass policy:
deny contains "User is anonymous" if not authenticated_user
authenticated_user if input.user_id != "anonymous"
For every not X check, ensure X evaluates to true or is undefined — never errors.
Prefer helpers over comprehensions
Partial helper rules are more debuggable, reusable, and testable than inline comprehensions.
Unconditional assignments in the rule head
full_name := concat(", ", [input.first_name, input.last_name])
Variables & Data Types
Membership and iteration
allow if "admin" in input.user.roles
internal_hosts contains hostname if {
some host in data.network.hosts
host.internal == true
hostname := host.name
}
Universal quantification
allow if {
every container in input.request.object.spec.containers {
not startswith(container.image, "old.docker.registry/")
}
}
Assignment vs comparison
:=for assignment,==for comparison. Avoid=except for pattern matching.- Always declare variables with
someor:=.
Sets over arrays
Sets for unordered unique collections (O(1) lookups, set operations):
required_roles := {"accountant", "reports-writer"}
allow if required_roles & provided_roles == required_roles
Functions
- Depend only on arguments, not
input,data, or other rules. - Use
:=for return values.
Documentation
Use metadata annotations:
# METADATA
# title: Deny non admin users
# description: Only admin users are allowed to access these resources
# custom:
# code: 401
# error_id: E123
Packages & Imports
- Package name matches file location.
- Import packages, not individual rules:
import data.user allow if user.is_admin - Don't import from
input— keep the data source obvious.
Debugging
print() for trace output
allow if {
print("user:", input.user, "roles:", input.user.roles)
"admin" in input.user.roles
}
print() writes to stderr during opa eval and opa test -v. Remove before production.
opa eval with explain
# Show full evaluation trace
opa eval --data policy.rego --input input.json "data.authz.allow" --explain=full
# Format output for readability
opa eval --data policy.rego --input input.json "data.authz.allow" --format=pretty
Common reasons for unexpected undefined
- Missing input field —
input.user.roleis undefined wheninput.userdoesn't exist. Guard withinput.usercheck first or use default values. - Typo in field name — Rego doesn't error on missing fields, just returns undefined. Use
opa check --strictto catch unused variables. - Type mismatch — comparing string
"80"to number80silently fails. Useto_number()or ensure consistent types. - Negation on undefined —
not xis true whenxis undefined AND whenxis false. Be explicit about what you're negating.
Performance
OPA optimizes most patterns automatically, but a few things matter at scale:
- Use indexing. OPA indexes rules with equality (
==),in, and glob comparisons. Structure hot-path rules so the first condition uses an indexed lookup:# Good — OPA indexes on input.request.kind.kind deny contains msg if { input.request.kind.kind == "Pod" # ... rest of conditions } - Avoid
http.sendin hot paths. External calls add latency and can fail. Prefer loading external data via bundles or pushing data to OPA's in-memory store. - Prefer sets over arrays for lookups.
x in setis O(1),x in arrayis O(n). - Profile with
opa eval --profileto find slow rules:opa eval --data policy/ --input input.json --profile --format=pretty "data.authz.allow" - Benchmark with
opa benchto measure evaluation time:opa bench --data policy/ --input input.json "data.authz.allow" - Partial evaluation (
opa eval --partial) precomputes rules when parts of input are unknown — useful for generating optimized policies for downstream enforcement.
Bundles and Decision Logging
Bundles
Bundles are the standard way to distribute policies and data in production. OPA periodically polls a bundle server (S3, GCS, HTTP) for updates.
# OPA config (opa-config.yaml)
services:
bundle-server:
url: https://bundle-server.example.com
bundles:
authz:
service: bundle-server
resource: bundles/authz.tar.gz
polling:
min_delay_seconds: 10
max_delay_seconds: 30
Build a bundle: opa build -b policy/ -o bundle.tar.gz. Bundles include .rego files, data.json, and an optional .manifest for roots.
Decision Logging
Decision logs provide an audit trail of every policy evaluation — who asked, what input, what result.
decision_logs:
service: log-server
reporting:
min_delay_seconds: 5
max_delay_seconds: 10
mask_decision: /system/log/mask # policy to redact sensitive fields
Mask sensitive fields to avoid logging PII or secrets:
package system.log
mask contains "/input/password"
mask contains "/input/token"
New policy workflow
- [ ] Design policy rules and identify input schema
- [ ] Write deny/allow rules with undefined value handling
- [ ] Extract complex conditions into helper rules
- [ ] Write tests (positive, negative, missing input)
- [ ] Run validation loop (below)
Linting with Regal
Regal checks 7 rule categories:
- bugs — common mistakes and inefficiencies
- idiomatic — non-idiomatic Rego constructs
- imports — import statement issues
- performance — suboptimal patterns
- style — style guide violations
- testing — test quality issues
- custom — organization-specific rules
Configure per-project in .regal/config.yaml. Use regal lint --format json for CI.
Validation loop
opa fmt --write— auto-formatopa check --strict .— fix any type errorsregal lint .— fix linter warnings (see categories above)opa test . -v— fix failing tests- Repeat until all four pass clean
Deep-dive references
Deny rule patterns: See patterns/deny-rules.md for RBAC, resource constraints, network rules Kubernetes policies: See patterns/kubernetes-policies.md for Gatekeeper, admission control Testing: See patterns/testing-patterns.md for table-driven tests, mocks, edge cases Built-ins: See builtins-cheatsheet.md for grouped OPA built-in functions with examples
Official references
- OPA Rego Style Guide — naming, rules, variables, functions, imports
- OPA Built-in Functions — full 150+ function reference
- Regal Linter — rule categories, configuration, editor integration