OPA/Rego Policy Engine
How iddio integrates Open Policy Agent for enterprise-grade policy evaluation. Build-tag gating, the PolicyEvaluator interface, shadow mode for safe rollouts, and fail-closed semantics.
Why OPA
Iddio’s built-in policy engine uses YAML rules. For most deployments, YAML is enough — it covers agent-scoped namespace access, tier mappings, and runbook assignments. But enterprise customers need more: policies that reference external data, time-based conditions, cross-cutting constraints, and the ability to express logic that YAML can’t represent.
Open Policy Agent (OPA) with its Rego policy language is the industry standard for this. Rather than building a custom expression language, iddio integrates OPA as an optional policy evaluator.
Build-Tag Gating
OPA adds a significant dependency tree. Most iddio users don’t need it. To keep the default binary small and dependency-free, OPA support is gated behind a Go build tag:
//go:build opa
package policy
import "github.com/open-policy-agent/opa/rego"
Build without OPA (default):
go build ./cmd/iddio
# Binary: ~12MB, no OPA dependency
Build with OPA:
go build -tags opa ./cmd/iddio
# Binary: ~28MB, includes OPA evaluator
The build tag is checked at compile time. If you build without opa, the OPA evaluator type doesn’t exist in the binary at all — not as dead code, but literally absent.
The PolicyEvaluator Interface
Iddio defines a PolicyEvaluator interface that both the YAML engine and OPA engine implement:
type PolicyEvaluator interface {
Evaluate(ctx context.Context, input EvalInput) (EvalResult, error)
}
type EvalInput struct {
Agent string
Method string
Path string
Resource string
Namespace string
Tier int
Timestamp time.Time
Labels map[string]string
}
type EvalResult struct {
Decision string // "allow", "deny", "escalate"
Reason string
Duration time.Duration
}
The proxy doesn’t know which evaluator is active. It calls Evaluate and acts on the result.
OPA Evaluator Implementation
The OPA evaluator loads Rego policies from a configurable directory, prepares a query, and evaluates it against the request input:
type OPAEvaluator struct {
query rego.PreparedEvalQuery
}
func NewOPAEvaluator(policyDir string) (*OPAEvaluator, error) {
r := rego.New(
rego.Query("data.iddio.authz.decision"),
rego.Load([]string{policyDir}, nil),
)
query, err := r.PrepareForEval(context.Background())
if err != nil {
return nil, fmt.Errorf("opa prepare: %w", err)
}
return &OPAEvaluator{query: query}, nil
}
Example Rego Policy
A Rego policy for iddio follows a simple convention: it must produce a decision object with action and reason fields:
package iddio.authz
default decision = {"action": "deny", "reason": "no matching rule"}
# Allow all reads
decision = {"action": "allow", "reason": "read operation"} {
input.tier == 0
}
# Allow T1 operations during business hours
decision = {"action": "allow", "reason": "business hours runbook"} {
input.tier == 1
hour := time.clock(time.now_ns())[0]
hour >= 9
hour < 18
}
# Escalate writes in production namespaces
decision = {"action": "escalate", "reason": "production write"} {
input.tier >= 2
startswith(input.namespace, "prod")
}
# Deny break-glass in all namespaces
decision = {"action": "deny", "reason": "break-glass denied"} {
input.tier == 4
}
Shadow Mode
Rolling out OPA policies in production is risky — a typo in Rego can block all traffic. Shadow mode lets you run the OPA evaluator alongside the YAML engine without affecting traffic:
policy:
evaluator: yaml # primary (makes decisions)
shadow_evaluator: opa # shadow (logged, not enforced)
opa:
policy_dir: /etc/iddio/opa/
In shadow mode:
- Both evaluators receive every request
- The YAML evaluator’s decision is enforced
- The OPA evaluator’s decision is logged alongside it
- Disagreements are flagged in the audit log
{
"agent": "claude-code",
"tier": 2,
"decision": "escalate",
"shadow_decision": "allow",
"shadow_evaluator": "opa",
"shadow_divergence": true
}
This lets you validate OPA policies against real traffic before switching over. When you’re confident the OPA policy produces correct results, change evaluator: opa and remove the shadow.
Fail-Closed Semantics
If the OPA evaluator encounters an error — policy compilation failure, evaluation timeout, or unexpected result format — it returns deny:
func (o *OPAEvaluator) Evaluate(ctx context.Context, input EvalInput) (EvalResult, error) {
results, err := o.query.Eval(ctx, rego.EvalInput(input))
if err != nil {
return EvalResult{Decision: "deny", Reason: "opa evaluation error"}, nil
}
if len(results) == 0 {
return EvalResult{Decision: "deny", Reason: "no opa result"}, nil
}
// Parse result...
}
This is fail-closed by design. A broken policy never results in unintended access. The error is logged, and the operator can see exactly what went wrong.
Hot Reload
OPA policies are hot-reloaded using the same fsnotify-based mechanism as YAML policies. When a .rego file in the policy directory changes, the evaluator recompiles and swaps atomically. Compilation errors trigger the last-known-good fallback.
Try It Yourself
Iddio is open source. Deploy a zero-trust command proxy for your AI agents in minutes.