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.
99
Quality
Pending
Does it follow best practices?
Impact
99%
1.19xAverage score across 31 eval scenarios
Pending
The risk profile of this skill
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.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.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.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.docs
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