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.
Prerequisites
Section titled “Prerequisites”- OPA installed locally for testing (
brew install opaor the OPA releases page). - A Caracal zone with an active policy set (see Activate a Policy Set).
Package and rule requirements
Section titled “Package and rule requirements”Every policy must:
- Declare
package caracal.authz. - Import
rego.v1. - Define a
resultrule that produces the required output object.
package caracal.authzimport rego.v1
result := { ... }The result object
Section titled “The result object”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": [],}| Field | Type | Description |
|---|---|---|
decision | "allow" | "deny" | The access decision |
evaluation_status | "complete" | Any value other than "complete" is treated as an evaluation error |
determining_policies | array | Objects identifying which policy rules drove the decision |
diagnostics | array | Arbitrary objects surfaced to the SDK; used for step-up signaling |
The OPA input object
Section titled “The OPA input object”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).
Forbidden builtins
Section titled “Forbidden builtins”The following OPA builtins are blocked and cause an evaluation error if used:
http.sendnet.lookup_ip_addrnet.cidr_contains,net.cidr_intersects,net.cidr_merge,net.cidr_expandopa.runtimerand.intntime.now_ns
Policies must be deterministic and must not make network calls. Use data.* for reference material.
Default deny with explicit allow
Section titled “Default deny with explicit allow”This pattern ensures the policy always produces a valid result, with allow overriding deny:
package caracal.authzimport 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"}Scope-based access
Section titled “Scope-based access”Check that requested scopes are covered by what the principal is allowed:
package caracal.authzimport rego.v1
default result := { "decision": "deny", "evaluation_status": "complete", "determining_policies": [], "diagnostics": [],}
# Allowed scopes per resource — load from data for real policiesallowed_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 }}Step-up trigger
Section titled “Step-up trigger”Emit a step_up_required diagnostic to signal that the STS should create a step-up challenge:
package caracal.authzimport rego.v1
sensitive_resources := {"resource://payments", "resource://transfers"}
# Allow once challenge is resolvedresult := { "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 resolvedresult := { "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 elsedefault 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.
Reference data with data.*
Section titled “Reference data with data.*”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_vendorsresult := { "decision": "allow", ...} if { input.principal.id in data.approved_vendors}Test locally with OPA
Section titled “Test locally with OPA”Save a policy to policy.rego and an input to input.json, then evaluate:
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": [] } } ] } ]}What to read next
Section titled “What to read next”- Activate a Policy Set — upload the policy, bundle it into a policy set, and make it active
- Concepts: Policy — how policies are versioned, bundled, and evaluated