Skip to content

Author a Rego Policy

Caracal policies are Rego modules evaluated by OPA. Every token exchange calls OPA once per requested resource and expects a single result object back. The policy decides whether to allow or deny the exchange and may attach diagnostic data that the SDK and STS use for step-up challenge signaling.

Every policy must:

  1. Declare package caracal.authz.
  2. Import rego.v1.
  3. Define a result rule that produces the required output object.
package caracal.authz
import rego.v1
result := { ... }

The result rule must produce an object with exactly these four fields:

result := {
"decision": "allow", # or "deny"
"evaluation_status": "complete",
"determining_policies": [{"policy": "my-policy-name"}],
"diagnostics": [],
}
FieldTypeDescription
decision"allow" | "deny"The access decision
evaluation_status"complete"Any value other than "complete" is treated as an evaluation error
determining_policiesarrayObjects identifying which policy rules drove the decision
diagnosticsarrayArbitrary objects surfaced to the SDK; used for step-up signaling

The STS populates input on every evaluation:

{
"principal": {
"type": "application",
"id": "app-abc123",
"zone_id": "my-zone",
"credential_type": "token",
"agent_session_id": "sess-xyz"
},
"resource": {
"type": "resource",
"id": "res-def456",
"identifier": "resource://payments",
"scopes": ["payment:read", "payment:submit"]
},
"action": {
"id": "token_exchange"
},
"session": {
"id": "sess-xyz"
},
"delegation_edge": {
"id": "edge-ghi789",
"source_session_id": "sess-parent",
"target_session_id": "sess-xyz",
"issuer_application_id": "app-orchestrator",
"receiver_application_id": "app-abc123",
"resource_id": "res-def456",
"scopes": ["payment:submit"],
"edge_version": 1,
"path": ["edge-ghi789"],
"graph_epoch": 42,
"constraints_json": {}
},
"context": {
"actor_claims": {
"roles": ["analyst"],
"department": "finance",
"clearance_level": 3
},
"subject_claims": {},
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"session_id": "sess-xyz",
"agent_session_id": "sess-xyz",
"delegation_edge_id": "edge-ghi789",
"challenge_resolved": false,
"requested_scopes": ["payment:submit"]
}
}

delegation_edge is present only when the exchange involves a delegation edge. context.actor_claims carries custom claims injected by the credential provider (e.g., from an OIDC userinfo endpoint).

The following OPA builtins are blocked and cause an evaluation error if used:

  • http.send
  • net.lookup_ip_addr
  • net.cidr_contains, net.cidr_intersects, net.cidr_merge, net.cidr_expand
  • opa.runtime
  • rand.intn
  • time.now_ns

Policies must be deterministic and must not make network calls. Use data.* for reference material.

This pattern ensures the policy always produces a valid result, with allow overriding deny:

package caracal.authz
import rego.v1
default result := {
"decision": "deny",
"evaluation_status": "complete",
"determining_policies": [],
"diagnostics": [],
}
result := {
"decision": "allow",
"evaluation_status": "complete",
"determining_policies": [{"policy": "allow-rule"}],
"diagnostics": [],
} if {
allow
}
default allow := false
allow if {
"analyst" in input.context.actor_claims.roles
input.resource.identifier == "resource://reports"
}

Check that requested scopes are covered by what the principal is allowed:

package caracal.authz
import rego.v1
default result := {
"decision": "deny",
"evaluation_status": "complete",
"determining_policies": [],
"diagnostics": [],
}
# Allowed scopes per resource — load from data for real policies
allowed_scopes := {"resource://inventory": {"inventory:read", "inventory:write"}}
result := {
"decision": "allow",
"evaluation_status": "complete",
"determining_policies": [{"policy": "scope-check"}],
"diagnostics": [],
} if {
permitted := allowed_scopes[input.resource.identifier]
every scope in input.context.requested_scopes {
scope in permitted
}
}

Emit a step_up_required diagnostic to signal that the STS should create a step-up challenge:

package caracal.authz
import rego.v1
sensitive_resources := {"resource://payments", "resource://transfers"}
# Allow once challenge is resolved
result := {
"decision": "allow",
"evaluation_status": "complete",
"determining_policies": [{"policy": "payments-policy"}],
"diagnostics": [],
} if {
input.resource.identifier in sensitive_resources
input.context.challenge_resolved == true
}
# Deny and request step-up when challenge not yet resolved
result := {
"decision": "deny",
"evaluation_status": "complete",
"determining_policies": [{"policy": "payments-policy"}],
"diagnostics": [{"step_up_required": "mfa"}],
} if {
input.resource.identifier in sensitive_resources
not input.context.challenge_resolved
}
# Default deny for everything else
default result := {
"decision": "deny",
"evaluation_status": "complete",
"determining_policies": [],
"diagnostics": [],
}

The step_up_required value ("mfa", "human_approval", or "software_attestation") determines the challenge type the STS creates.

Store reference tables outside policy logic by loading them as OPA data documents. When you version a policy set, bundle the policy and a data document together. Access via data.my_table:

# Policy references data.approved_vendors
result := {
"decision": "allow",
...
} if {
input.principal.id in data.approved_vendors
}

Save a policy to policy.rego and an input to input.json, then evaluate:

Terminal window
opa eval \
--input input.json \
--data policy.rego \
'data.caracal.authz.result'

A well-formed allow result looks like:

{
"result": [
{
"expressions": [
{
"value": {
"decision": "allow",
"evaluation_status": "complete",
"determining_policies": [{"policy": "allow-rule"}],
"diagnostics": []
}
}
]
}
]
}