CoursesOPA & RegoIntermediate

Deny/allow patterns

The idioms real policies use.

Advanced14 min · lesson 5 of 12

Real-world Rego settles on a few idioms. The most common is the deny set: a rule deny contains msg that accumulates a message for every violation found, so evaluating deny yields the full list of problems (empty means compliant). This is what Conftest and Gatekeeper expect. It reads naturally — “add a denial message when this bad condition holds” — and iterating with [_] lets one rule check every element.

CHOOSING THE POLICY SHAPE
deny-set (validation)
deny contains msg
add a message per violation
iterate with [_]
check every element
empty set = compliant
what Conftest/Gatekeeper expect
allow + default (authz)
default allow := false
deny by default
allow if { ... }
grant explicitly
no rule matches = denied
safe posture
structured (rich decision)
return an object
beyond boolean
allowed / reasons
why it was decided
remediation
how to fix
Pick the shape by use case: deny-set for validation, allow-with-default for authz, structured for rich responses.
policy.rego
package main
deny contains msg if {
input.kind == "Service"
input.spec.type == "LoadBalancer"
not input.metadata.annotations["acme.io/public-approved"]
msg := sprintf("Service %v is a public LoadBalancer without approval", [input.metadata.name])
}

allow, default, and full-decision policies

For authorization you often want the opposite shape: default allow := false, then allow if { ... } rules that grant access — deny-by-default, allow explicitly. And beyond boolean, a policy can return a structured decision (an object with allowed, reasons, and remediation) so the caller gets rich feedback. Choosing the shape — deny-set for validation, allow-with-default for authz, structured for detailed responses — is most of designing a policy.

authz.rego
package authz
default allow := false # deny by default
allow if {
input.user.roles[_] == "editor"
input.action == "write"
startswith(input.resource, "docs/")
}
Default deny, or a gap becomes an allow
For authorization, always default allow := false and grant explicitly. Without the default, an input that matches no rule yields undefined rather than false, and a caller that treats “not true” as “allow” (or mishandles undefined) silently permits it. Deny-by-default with explicit grants is the only safe posture; the same care applies to how the enforcing service interprets the decision.