Rego is the declarative policy language used by Open Policy Agent (OPA). This tile covers writing and testing Rego policies for Kubernetes admission control, Terraform and infrastructure-as-code plan validation, Docker container authorization, HTTP API authorization, RBAC and role-based access control, data filtering, metadata annotations with opa inspect, and OPA policy testing with opa test.
96
94%
Does it follow best practices?
Impact
97%
1.18xAverage score across 32 eval scenarios
Low
Low-risk findings worth noting
All rules in this document MUST be followed when generating or modifying Rego policy code.
Write tests before implementing policies (TDD): Create the _test.rego file with test cases BEFORE writing the policy. Run opa test to confirm tests fail, then implement the policy until all tests pass. Every .rego file must have a corresponding _test.rego — a policy without tests is incomplete.
Never run Terraform commands: Do NOT run terraform plan, terraform apply, terraform init, or any other Terraform CLI command. Create mock plan JSON inline in _test.rego using the with keyword, or ask the user to provide plan output.
Use import rego.v1: Always include import rego.v1 to opt in to v1 syntax (if, contains, in, every keywords). Never use import future.keywords — it is deprecated and cannot be combined with import rego.v1. Enforced by Regal use-rego-v1.
Never import input: Keep input references explicit (e.g., input.resource.tags). For Terraform IaC policies, never use import input as tfplan — normalise with tfplan := object.get(input, "plan", input) instead. Enforced by Regal avoid-importing-input.
Every package and every entrypoint rule MUST have a # METADATA annotation block. The block must be placed immediately before the target with no intervening lines — Regal detached-metadata flags any gap. Regal missing-metadata flags packages or rules without annotations, and no-defined-entrypoint flags policies with no entrypoint rule.
Package-level annotation (place before package declaration):
# METADATA
# title: My Policy
# description: What this policy enforces
# authors:
# - Team Name <team@example.com>
# custom:
# category: kubernetes-admission
package my.policy
import rego.v1Rule-level annotation (place immediately before the rule):
# METADATA
# title: Allow compliant requests
# description: Permits requests that pass all validation checks
# entrypoint: true
# custom:
# severity: HIGH
allow if {
count(deny) == 0
}Required fields for entrypoint rules: title, description, entrypoint: true, custom.severity (HIGH, MEDIUM, or LOW). Enforced by Regal invalid-metadata-attribute and annotation-without-metadata.
Do NOT add entrypoint: true to Conftest policies — Conftest queries rules by naming convention (deny, warn, violation) and does not use OPA's entrypoint mechanism.
Runtime access: Use rego.metadata.rule() to read a rule's own annotation at evaluation time (e.g., for severity-aware error formatting). No mocking needed — the metadata is baked into the policy source.
violation contains msg if {
# ... violation logic ...
meta := rego.metadata.rule()
msg := {"message": "...", "severity": meta.custom.severity}
}Schema annotations: Associate JSON schemas with input and data paths to enable opa check structural validation at build time.
Standard layout — organise every module in this order:
# METADATA block (package-level)package declarationimport statementsdefault declarations (always first, before the rules they apply to)Default declarations first: Declare default allow := false and any other defaults at the top of the rules section, before the rule bodies that define when they are true. Regal trailing-default-rule flags defaults that appear after the rules they apply to.
Imports before rules: All import statements must appear before any rule declarations. Regal import-after-rule flags imports that follow rules.
Package mirrors directory: foo/bar/policy.rego → package foo.bar.policy. Regal directory-package-mismatch flags mismatches.
Separate concerns by package: Keep related rules in the same package. Use separate packages for different policy domains.
snake_case for all identifiers: Rules, functions, variables, and constants — no camelCase or PascalCase. Regal prefer-snake-case.get_ or list_ prefixes: These are implied by Rego semantics. Use user, not get_user. Regal avoid-get-and-list-prefix.kubernetes.admission, don't name a rule kubernetes_admission_deny. Regal rule-name-repeats-package.is_ or has_ prefix for boolean helpers: is_privileged, has_required_labels._ prefix for internal helpers: Communicates that a rule is not part of the public API.test_, describe what's being tested — test_deny_privileged_container.foo, bar, baz, tmp — use meaningful names. Regal metasyntactic-variable.Use if keyword: Separates rule head from body. Required with import rego.v1. Regal use-if.
Use contains for partial set rules: deny contains msg if { ... } not deny[msg] { ... }. Regal use-contains.
Use in for membership checks: "admin" in input.user.roles not "admin" == input.user.roles[_]. Regal use-in-operator.
Use some x in collection for iteration: Not x := collection[_]. Regal prefer-some-in-iteration. Don't mix iteration styles in the same rule — Regal mixed-iteration.
Declare output variables with some: When a variable is assigned inside a rule body for output (not just local use), declare it with some. Regal use-some-for-output-vars.
Don't use some unnecessarily: some x in arr is correct; some x alone before x := value is unnecessary when x is not an output variable. Regal unnecessary-some.
default over else: Use default rule := value for fallback values — the same pattern as default allow := false, applied to any rule. Declare the default first, then the specific rule heads. Do not use else :=. Regal default-over-else.
default user_limit := 10 # fallback first
user_limit := 1000 if data.user_tiers[input.user] == "premium"
user_limit := 100 if data.user_tiers[input.user] == "standard"default over negation: Prefer default allow := false over allow if not is_denied. Regal default-over-not.
Use every for universal quantification: Express "for all" logic clearly instead of double-negation. every container in spec.containers { not container.securityContext.privileged }. Regal double-negative.
Boolean assignment via if: Write is_valid if { ... } not is_valid := true if { ... }. Regal boolean-assignment.
Prefer set/object rules over comprehensions: violations contains v if { ... } (multi-value rule) over violations := { v | ... } (top-level comprehension) — rules can be extended across files. Regal prefer-set-or-object-rule.
Move assignment to comprehension term: {v.name | some v in input.items; v.active} not {name | some v in input.items; v.active; name := v.name}. Regal comprehension-term-assignment.
Pattern matching in function arguments: Use f("foo") := ... over f(x) := ... if x == "foo" when matching on a literal. Regal equals-pattern-matching.
Use object.keys over manual key extraction: object.keys(obj) not {k | obj[k]}. Regal use-object-keys.
Use array.flatten and object.union_n: Prefer built-ins over nested array.concat or repeated object.union calls. Regal use-array-flatten, use-object-union-n.
Use raw strings for regex: regex.match(`[\d]+`, s) not regex.match("[\\d]+", s). Regal non-raw-regex-pattern.
Prefer ==/!= over count for empty checks: count(violations) == 0 → violations == set(). Regal equals-over-count.
No wildcard key with in: some v in collection not some _, v in collection when the key is unused. Regal in-wildcard-key.
Avoid single-item in: input.user == "alice" not input.user in {"alice"}. Regal single-item-in.
Use strings.count where possible: Prefer strings.count(s, sub) over manual counting patterns. Regal use-strings-count.
No external references in functions: Functions must operate only on their arguments — never reference input or data directly inside a function body. Pass data as explicit arguments. Regal external-reference.
# Wrong — references input directly
is_admin := input.user.role == "admin"
# Correct — takes role as argument
is_admin(role) := role == "admin"Functions must have arguments: Zero-argument functions that return a value should be rules instead. Regal zero-arity-function.
Don't assign return value in argument: Use result := f(x) not f(x, result) (last-argument return pattern). Regal function-arg-return.
Consistent argument names across function heads: If a function has multiple heads, argument names must be consistent. Regal inconsistent-args.
Arguments are not always wildcards: If every call to a function passes _ for an argument, the argument serves no purpose — remove it. Regal argument-always-wildcard.
import data.my.package then package.rule — not import data.my.package.rule. Regal prefer-package-imports.import data.foo as foo is pointless — omit the alias. Regal redundant-alias.import data is implicit — never write it. Regal redundant-data-import._test.rego suffix: Test files must be named policy_test.rego (not policy.test.rego or test_policy.rego). Regal file-missing-test-suffix._test package + import policy: Test packages end in _test (e.g., package my.policy_test). Import the policy under test and reference rules via the alias — bare rule names are not in scope. Regal test-outside-test-package.test_ prefix on all test rules: Every test function starts with test_. Regal enforces this via test discovery.todo_test_ in production: todo_test_ marks unimplemented tests (reported as SKIPPED). Remove or implement before shipping. Regal todo-test.print or trace in production code: These are debugging tools only. Regal print-or-trace-call, dubious-print-sprintf.with outside tests: with overrides are for test mocking only — not for production logic. Regal with-outside-test-context.input JSON from memory or intuition — check real terraform show -json output (or the provider's schema/docs) for how a field is actually represented. HCL repeatable blocks (foo { ... }) serialize as a list of 0-or-1 objects in plan JSON, not a bare object — a mock like "capabilities": {"drop": ["ALL"]} will pass tests while silently not matching production input shaped "capabilities": [{"drop": ["ALL"]}]. When in doubt, generate one real plan and copy the actual shape into fixtures instead of guessing.allow if 1 == 1 is meaningless. Every condition must check something meaningful. Regal constant-condition.sprintf argument count: sprintf("hello %v %v", [x]) with mismatched args will produce wrong output. Regal sprintf-arguments-mismatch.not: not x where x can never be true is always true — a logic error. Regal impossible-not.x != null before accessing x.field if OPA will already fail safely on undefined. Regal redundant-existence-check.!= in loops: some x in arr; x != "foo" doesn't mean "no element equals foo" — it matches all elements except "foo". Use every or not instead. Regal not-equals-in-loop.x := input.items[_] at package level fails when there are multiple items. Iteration belongs inside rule bodies. Regal top-level-iteration.print, count, array, etc. Regal rule-shadows-builtin, var-shadows-builtin._ are internal — don't reference them from other packages. Regal leaked-internal-reference.x := f(y) not just f(y). Regal unassigned-return-value.count(arr) > 0 before some x in arr is redundant — the loop body simply won't execute if empty. Regal redundant-loop-count.default x := false, don't write a rule head x := false if { ... } — it's a no-op. Regal rule-assigns-default.if {} with empty object: allow if {} is a syntax error — empty objects after if are not rule bodies. Regal if-empty-object, if-object-literal._ cannot be marked entrypoint: true. Regal internal-entrypoint.if: if is a keyword in OPA 1.0 — naming a rule if is a parse error. Regal rule-named-if.time.now_ns() once: Cache the result in a variable — calling it twice in the same rule may return different values. Regal time-now-ns-twice.any(), all(), re_match(), net.cidr_overlap(), set_diff(), and all cast_*() functions. Use modern replacements (regex.match, net.cidr_contains, etc.). Regal deprecated-builtin.object.get on it: object.get(x, "key", default) only falls back to default when x is missing the key or x is itself undefined — it does not protect against x being the wrong type (e.g. a list instead of an object). Terraform providers frequently represent a "block" attribute (HCL foo { ... }) as a list of 0-or-1 objects in plan JSON, not a bare object — e.g. docker_container.capabilities is [] or [{"drop": [...]}], never {}. Calling object.get(capabilities, "drop", []) when capabilities is [] silently fails to match (no error, just an unsatisfied body) instead of raising a type error — the rule quietly stops firing instead of crashing, which is easy to miss in review and in tests that mock the field with the wrong shape. Before reading a nested field with object.get, check the field's actual shape in real terraform show -json output (or provider docs) — if it's a repeatable HCL block, iterate it as a list (some item in object.get(parent, "field", [])) rather than treating it as a map.opa fmt: All files must be formatted with opa fmt. Use opa fmt --write in CI. Regal opa-fmt.:= for assignment: Never use = for assignment or comparison — := assigns, == compares, = is unification (pattern matching only). Regal use-assignment-operator.== not = for equality: Prefer == over = for equality comparisons. Regal prefer-equals-comparison.x == "value" not "value" == x. Regal yoda-condition.# comment not #comment. Regal no-whitespace-comment.x := 1 with no condition belongs in the rule head, not the body. Regal unconditional-assignment.walk calls: If you only need values (not paths) from walk, use walk(x, [_, v]) and ignore the path. Regal walk-no-path.default allow := false and enumerate only safe conditions.deny contains msg if { ... } then gate allow on count(deny) == 0.{field | input.body[field]} - allowed_fields != set() detects disallowed keys without iterating field by field.object.get for safe access with defaults: object.get(obj, "key", default_value) avoids undefined when a key may be absent — but only guards absence, not the wrong type. Confirm obj is actually an object (not a list) before relying on it; see Bug Avoidance.with in tests: rule with input as mock with data.x as mock_data — never use with in production rules.data. Reference from policy rules.kubernetes.admission, rbac.authz, terraform.analysis — lowercase dot-separated hierarchy.opa test, opa check --strict, regal lint) before deployment.regal lint enforces all rules above automatically. Integrate as a required check..tessl-plugin
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
skills
rego-domain-reference
references