Rego Policy Development
You are an expert in Open Policy Agent (OPA) and the Rego policy language.
Mandatory Workflow
ALWAYS follow this sequence for any policy task:
- Understand - Clarify requirements before writing code
- Generate - Write policy with explicit default deny
- Test - Create comprehensive
*_test.regowith allow AND deny cases - Validate - Run
opa checkandopa test . -v - Review - Check against security checklist
- Iterate - Fix any failures before declaring complete
NEVER skip the test step. Every policy must have tests that pass.
Quick Reference
| Task | Guide |
|---|---|
| Generate policy | Follow GENERATE.md |
| Security review | Check SECURITY.md |
| Write tests | Follow TESTING.md |
| Best practices | See BEST-PRACTICES.md |
Core Principles
1. Always Default Deny
Every policy MUST start with explicit default deny:
package mypackage
import rego.v1
default allow := false
allow if {
# explicit conditions only
}
2. Modern Rego Syntax (OPA 1.0+)
On OPA 1.0+ the if / in / contains / every keywords are built in — no import
needed. (import rego.v1 and import future.keywords.* are now no-ops; keep import rego.v1
only if you must also run on OPA 0.x.) See BEST-PRACTICES.md for the full
1.0 migration notes.
package authz
# No import needed on OPA 1.0+.
# Use 'if' for rule bodies
allow if {
some role in input.user.roles
role == "admin"
}
# Use 'contains' for set rules
violations contains msg if {
# condition
msg := "violation message"
}
# Use 'every' for universal checks
all_valid if {
every item in input.items {
item.status == "approved"
}
}
3. Structured Decisions
Return structured objects for better debugging:
decision := {
"allowed": allowed,
"reason": reason,
"context": {
"user": input.user.id,
"action": input.action
}
}
4. Always Write Tests
Every policy needs a companion *_test.rego file:
package mypackage_test
import rego.v1
import data.mypackage
test_allow_admin if {
mypackage.allow with input as {
"user": {"roles": ["admin"]}
}
}
test_deny_guest if {
not mypackage.allow with input as {
"user": {"roles": ["guest"]}
}
}
Validation Commands
Always validate your work:
# Check syntax
opa check policy.rego
# Run tests
opa test . -v
# Format code
opa fmt -w policy.rego
# Test with coverage
opa test . -v --coverage
Common Patterns
RBAC (Role-Based Access Control)
package rbac
import rego.v1
default allow := false
allow if {
some role in input.user.roles
some permission in role_permissions[role]
permission == required_permission
}
role_permissions := {
"admin": ["read", "write", "delete"],
"editor": ["read", "write"],
"viewer": ["read"]
}
required_permission := "read" if input.action == "GET"
required_permission := "write" if input.action in ["POST", "PUT", "PATCH"]
required_permission := "delete" if input.action == "DELETE"
ABAC (Attribute-Based Access Control)
package abac
import rego.v1
default allow := false
# Owner can do anything with their resources
allow if {
input.user.id == input.resource.owner_id
}
# Department access
allow if {
input.user.department == input.resource.department
input.action in ["read", "list"]
}
API Gateway Authorization
package gateway
import rego.v1
default allow := false
allow if {
is_public_path
}
allow if {
is_authenticated
has_required_permission
}
is_public_path if {
some pattern in public_patterns
glob.match(pattern, [], input.path)
}
public_patterns := [
"/api/health",
"/api/public/*"
]
is_authenticated if {
input.token.valid == true
time.now_ns() < input.token.exp * 1e9
}
has_required_permission if {
required := path_permissions[input.method][_]
glob.match(required.pattern, [], input.path)
some role in input.token.roles
role in required.roles
}
Security Checklist
Before completing any policy:
- Default deny is explicit (
default allow := false) - No unconditional
allow := true - Input validation for required fields
- Type checking where needed (
is_string,is_array) - No path traversal vulnerabilities
- Tests cover allow AND deny cases
- Tests cover edge cases (null, empty, missing)
Detailed Guides
For comprehensive guidance, see:
- GENERATE.md - Step-by-step policy generation
- SECURITY.md - Security review checklist
- TESTING.md - Test patterns and coverage
- BEST-PRACTICES.md - Performance and style
Example Files
See examples/ directory for complete working examples:
rbac_test.rego- RBAC with testsgateway_test.rego- API gateway with tests
Suite-wide security audit (Workflow)
The generate / test / review loop above handles ONE policy at a time inline. To audit an entire
policy corpus for security drift, this skill ships a read-only fan-out workflow:
rego-security-audit-workflow.js. It spawns one auditor per .rego policy, scores each against the
10-check rubric below, adversarially verifies every failed check (so a misread doesn't become a
false alarm), runs a cross-policy conflict pass over same-package groups, and returns a dated report
payload. It is report-only — it never edits a policy. The skill (this wrapper) owns the date and
the git write/commit; the workflow owns the fan-out (it's read-only against git).
Requires a Claude Code harness with the
Workflow(multi-agent orchestration) tool. The inline generate / test / review loop works without it; only this corpus-audit needs it.
The 10 checks (each cites a SECURITY.md / BEST-PRACTICES.md section)
| Check | Source | What it verifies |
|---|---|---|
DEFAULT_DENY |
SECURITY §1 | Explicit default deny; no unconditional allow. |
INPUT_VALIDATION |
SECURITY §2 | Required fields checked; missing → deny not error; null/type handled. |
PRIV_ESCALATION |
SECURITY §3 | Strict-inequality role levels; self-mod blocked; protected roles unassignable. |
PATH_TRAVERSAL |
SECURITY §4 | Path/id inputs validated (.., /, %, \); no raw startswith. |
REDOS |
SECURITY §4 | No user-controlled regex; glob/literal preferred. |
DATA_EXPOSURE |
SECURITY §5 | Denial reasons don't leak roles/permissions/structure. |
TIME_BASED |
SECURITY §6 | Token exp/nbf checked before access (where tokens are handled). |
EVAL_CONFLICT |
BEST-PRACTICES | Competing rules mutually exclusive (whitelist guards / else-chains). |
DOMAIN_LOGIC_LEAK |
BEST-PRACTICES | Policy does authz only — no business/validation/workflow logic. |
TEST_COVERAGE |
TESTING | Companion *_test.rego covers allow + deny + edge cases. |
Conventions are NOT findings. Each policy is judged against ITS OWN idiom. Using
import future.keywordsinstead ofimport rego.v1, or returning{"allow": bool}instead of a bareallow, is recorded descriptively and never raised as a check failure. Only genuine, exploitable authorization defects are reported.
Running the audit
- [ ] 1. DATE=$(date +%F); ensure <root>/audit-reports/ exists.
- [ ] 2. Run the workflow (background; you're notified on completion).
- [ ] 3. Render the returned payload into <root>/audit-reports/REGO-SECURITY-AUDIT-<DATE>.md.
- [ ] 4. Commit the report to a branch (never the default branch). Do NOT push or open an MR unless asked.
- [ ] 5. Report the headline counts; offer to fix Critical/Medium items via the inline generate/review loop.
Step 2 — invoke:
Workflow({
scriptPath: "<this-skill-dir>/rego-security-audit-workflow.js",
args: { date: "<DATE>", policyRoot: "<dir-with-.rego-files>" } // policyRoot defaults to cwd
// optional: focus (steer) | policies:[explicit list] | maxPolicies (default 12)
})
The payload is { date, rubricVersion, scope:{audited,total,deferred,baselineTests,maxPolicies,policyRoot}, confirmed:[…severity-sorted, false alarms already dropped…], policyVerdicts:[…], crossPolicy:[…] }.
confirmed = findings that survived adversarial verification. crossPolicy = same-package
overlapping/shadowed-rule / eval_conflict issues a single-policy auditor can't see.
Date: YOU own the date — use the
$DATEyou computed for the filename, heading, and commit. If the payload'sdatereads"(undated)"(args didn't propagate), ignore it and stamp$DATEanyway.
Step 3 — report shape (REGO-SECURITY-AUDIT-<DATE>.md):
# Rego Security Audit — <DATE>
**Scope:** audited <audited> of <total> policies; baseline `<baselineTests>`. **<N> confirmed findings
(<C> Critical), <R> false alarms dropped, <X> cross-policy issues.**
## Confirmed findings (fix these) <!-- one ### block per item, Critical first -->
### [<severity>/<check>] <policy>
- **Evidence:** <evidence>
- **Rubric:** SECURITY/BEST-PRACTICES <section>
- _verifier:_ <reConfirm> <!-- or "unverified — confirm manually" if no verdict -->
## Cross-policy issues <!-- from crossPolicy[]; omit section if empty -->
### [<severity>/<kind>] <policies>
- <detail>
## Per-policy verdicts
| Policy | Package | Decision shape | Tests | Fails | N/A | Summary |
|--------|---------|----------------|-------|-------|-----|---------|
| … (one row per policyVerdicts entry) … |
If confirmed is empty, still write the report (a clean run is a useful record) and say so.
Step 4 — commit to a branch (never the default branch; no push/MR unless asked).
Incremental mode (optional)
The workflow carries a RUBRIC_VERSION. To audit only changed policies, the wrapper computes the set
whose git hash-object differs from the last report's manifest (or all, if RUBRIC_VERSION bumped),
and passes them as args.policies. First cut: full-suite every run (small corpora audit fast).
Notes & anti-patterns
- Report-only is the contract. This workflow finds; you fix (via the inline generate/review loop). An unattended agent "fixing" a large gateway policy is worse than a reported finding.
- The verify phase matters. A policy isn't insecure because one agent misread it — every Critical/Medium finding is adversarially re-checked, and false alarms are dropped before the report.
- Sizing: keep a run ≤ ~12 policies / ~50 agents.
maxPolicies(default 12) bounds it; deferred policies are listed inscope.deferredand re-surface on a later run.