# Writing Rego Policies

> Rego policy development best practices for OPA. Use when writing, modifying, or reviewing .rego files, OPA policies, policy rules, or policy tests.

- Skill: `chogos/writing-rego-policies` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add chogos/writing-rego-policies`
- Raw SKILL.md: https://api.skillmd.com/api/skills/chogos/writing-rego-policies/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Chogos (https://skillmd.com/u/chogos)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/chogos/writing-rego-policies

---


# Rego Development Best Practices

## Core Conventions

- OPA v1.0+ uses Rego v1 semantics by default (`if`/`contains` keywords required, duplicate imports prohibited). Use `import rego.v1` for compatibility across OPA versions — it's a no-op in v1.0+ and opts in on older versions.
- Use `--v0-compatible` flag when migrating legacy policies to v1.0.
- Always use `opa fmt` for consistent formatting. Use `opa fmt --rego-v1` to auto-rewrite policies to v1 syntax.
- Use `opa check` in build pipelines (strict mode is default in v1.0).
- Prioritize clear code over assumed performance optimizations — OPA handles most optimization automatically.

## Naming & Style

- `snake_case` for all rule names and variables (`user_is_admin`).
- Leading underscore for internal helpers (`_is_developer(user)`).
- Use descriptive rule names: `users` not `get_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:

```rego
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

```rego
full_name := concat(", ", [input.first_name, input.last_name])
```

## Variables & Data Types

### Membership and iteration

```rego
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

```rego
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 `some` or `:=`.

### Sets over arrays

Sets for unordered unique collections (O(1) lookups, set operations):
```rego
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:
```rego
# 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:
  ```rego
  import data.user
  allow if user.is_admin
  ```
- Don't import from `input` — keep the data source obvious.

## Debugging

### print() for trace output

```rego
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

```bash
# 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

1. **Missing input field** — `input.user.role` is undefined when `input.user` doesn't exist. Guard with `input.user` check first or use default values.
2. **Typo in field name** — Rego doesn't error on missing fields, just returns undefined. Use `opa check --strict` to catch unused variables.
3. **Type mismatch** — comparing string `"80"` to number `80` silently fails. Use `to_number()` or ensure consistent types.
4. **Negation on undefined** — `not x` is true when `x` is undefined AND when `x` is 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:
  ```rego
  # Good — OPA indexes on input.request.kind.kind
  deny contains msg if {
      input.request.kind.kind == "Pod"
      # ... rest of conditions
  }
  ```
- **Avoid `http.send` in 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 set` is O(1), `x in array` is O(n).
- **Profile with `opa eval --profile`** to find slow rules:
  ```bash
  opa eval --data policy/ --input input.json --profile --format=pretty "data.authz.allow"
  ```
- **Benchmark with `opa bench`** to measure evaluation time:
  ```bash
  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.

```yaml
# 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.

```yaml
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:

```rego
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](https://github.com/StyraInc/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

1. `opa fmt --write` — auto-format
2. `opa check --strict .` — fix any type errors
3. `regal lint .` — fix linter warnings (see categories above)
4. `opa test . -v` — fix failing tests
5. Repeat until all four pass clean

## Deep-dive references

**Deny rule patterns**: See [patterns/deny-rules.md](patterns/deny-rules.md) for RBAC, resource constraints, network rules
**Kubernetes policies**: See [patterns/kubernetes-policies.md](patterns/kubernetes-policies.md) for Gatekeeper, admission control
**Testing**: See [patterns/testing-patterns.md](patterns/testing-patterns.md) for table-driven tests, mocks, edge cases
**Built-ins**: See [builtins-cheatsheet.md](builtins-cheatsheet.md) for grouped OPA built-in functions with examples

## Official references

- [OPA Rego Style Guide](https://www.openpolicyagent.org/docs/latest/style-guide/) — naming, rules, variables, functions, imports
- [OPA Built-in Functions](https://www.openpolicyagent.org/docs/latest/policy-reference/#built-in-functions) — full 150+ function reference
- [Regal Linter](https://docs.styra.com/regal) — rule categories, configuration, editor integration

