Policy
A policy is a versioned Rego document that the STS evaluates at token-exchange time to determine whether to issue a mandate. This page covers the policy structure, the OPA evaluation model, policy sets, versioning, and the runtime loading behavior.
What a policy is
Section titled “What a policy is”A policy is a named Rego document with a fixed entry point. It receives structured input describing the principal, resource, session, and context, and must return a decision object at data.caracal.authz.result.
Policies are stored in the policies table, versioned in policy_versions (each version is immutable), and activated through a policy_set_binding on the zone.
The required entry point
Section titled “The required entry point”Every Caracal-compatible Rego policy must produce a result at data.caracal.authz.result:
package caracal.authz
import future.keywords.ifimport future.keywords.in
default result := { "decision": "deny", "evaluation_status": "complete", "determining_policies": [], "diagnostics": [],}
result := { "decision": "allow", "evaluation_status": "complete", "determining_policies": ["my-policy"], "diagnostics": [],} if { input.principal.type == "application" input.resource.identifier == "resource://example" "read" in input.resource.scopes}Required result fields:
| Field | Type | Description |
|---|---|---|
decision | string | "allow" or "deny" |
evaluation_status | string | Must be "complete" for the STS to accept the result |
determining_policies | string[] | IDs or names of the rules that determined the decision |
diagnostics | object[] | Arbitrary metadata; used for step-up signaling (see below) |
The OPA input object
Section titled “The OPA input object”The full input available to every policy:
{ "principal": { "type": "application", "id": "<application_id>", "zone_id": "<zone_id>", "credential_type": "token", "agent_session_id": "<agent_session_id>" }, "resource": { "type": "resource", "id": "<resource_id>", "identifier": "resource://payments", "scopes": ["transfer", "read"] }, "action": { "id": "TokenExchange" }, "session": { "id": "<session_id>" }, "delegation_edge": { "id": "<edge_id>", "source_session_id": "...", "target_session_id": "...", "issuer_application_id": "...", "receiver_application_id": "...", "resource_id": "...", "scopes": ["read"], "edge_version": 1, "path": ["<session_id_1>", "<session_id_2>"], "graph_epoch": 42, "constraints_json": { "max_hops": 1, "ttl_seconds": 300, "budget": ["read"] } }, "context": { "actor_claims": { "sub": "...", "zone_id": "...", "scope": "..." }, "subject_claims": { "sub": "...", "zone_id": "...", "scope": "..." }, "trace_id": "...", "session_id": "...", "agent_session_id": "...", "delegation_edge_id": "...", "challenge_resolved": false, "requested_scopes": ["read"] }}delegation_edge is present only when the exchange carries a delegation edge ID. session is present only when a session ID is provided.
Blocked builtins
Section titled “Blocked builtins”The following Rego builtins are disabled in all Caracal policies:
| Builtin | Reason |
|---|---|
http.send | Prevents network calls from policy evaluation |
net.lookup_ip_addr, net.cidr_*, net.cidr_expand | Prevents network inspection |
opa.runtime | Prevents runtime introspection |
rand.intn | Prevents nondeterministic decisions |
time.now_ns | Prevents clock-dependent decisions (use input.context timestamps if needed) |
Policies that reference any blocked builtin fail validation when submitted to the API.
Policy versioning
Section titled “Policy versioning”Each time a policy’s Rego source changes, a new version is created in policy_versions. Versions are immutable and identified by a content-addressable SHA-256 hash. The API validates Rego syntax and blocked builtins before accepting a new version.
Creating a policy version:
POST /zones/{zoneId}/policies/{policyId}/versions{ "content": "package caracal.authz\n...", "schema_version": "2026-03-16" }The schema_version field ("2026-03-16") identifies the input schema this policy was written for.
Policy sets and manifests
Section titled “Policy sets and manifests”A policy set groups one or more policy versions into a bundle evaluated together. The bundle is described by a manifest — an ordered list of policy_version_id entries. The STS computes a manifest_sha256 over the manifest content to cache invalidation.
Creating a policy set version:
POST /zones/{zoneId}/policy-sets/{id}/versions{ "manifest": [{ "policy_version_id": "<id>" }, ...] }Activation: A zone has one active policy set binding at a time. Activating a policy set version makes it the live enforcement bundle for all subsequent token exchanges in that zone.
POST /zones/{zoneId}/policy-sets/{id}/activate{ "version_id": "<version_id>", "shadow_version_id": "<optional_id>" }The optional shadow_version_id registers a shadow binding alongside the active one. The active binding determines all enforcement decisions; the shadow binding can be inspected for testing purposes.
Runtime loading
Section titled “Runtime loading”The STS loads policies lazily per zone:
- On first exchange for a zone, the STS queries the active policy set binding and compiles the Rego bundle into memory.
- Compiled bundles are cached in an atomic
opaZoneStateper zone. Cache hits are identified bymanifest_sha256— a bundle with the same SHA is not recompiled. - A background poll refreshes all known zones every 60 seconds as a safety net.
- The Redis stream
caracal.policy.invalidateis the fast path: the STS subscribes to this stream and reloads the affected zone immediately when a new binding is activated, bypassing the 60-second poll.
If policy compilation fails (syntax error, unsatisfiable rule, etc.), the STS installs the deny-all fallback for that zone. It does not crash. The error is visible in STS logs.
Signaling step-up from policy
Section titled “Signaling step-up from policy”A policy can signal that it requires elevated authentication before allowing exchange. It does this by returning a step_up_required entry in diagnostics:
result := { "decision": "deny", "evaluation_status": "complete", "determining_policies": ["payment-policy"], "diagnostics": [{"step_up_required": "mfa"}],} if { input.resource.identifier == "resource://high-value-payments" not input.context.challenge_resolved}When the STS detects step_up_required in diagnostics and the exchange has not already resolved a challenge, it creates a step-up challenge and returns HTTP 401 with a challenge object. See Step-Up Challenge for the full flow.
Next steps
Section titled “Next steps”- Authority Model — how the STS uses policy results to make decisions
- Principal and Application — what
input.principalcontains - Resource and Grant — what
input.resourcecontains - Step-Up Challenge — signaling elevated authentication from a policy