OPA: Policy Engine

Open Policy Agent (OPA) is a CNCF graduated project -- the industry standard for policy-as-code across the cloud-native ecosystem. PRECINCT uses OPA as its policy engine, enforcing fine-grained authorization on every AI agent request.

Every agent request in PRECINCT is evaluated against declarative, version-controlled policies written in Rego. OPA (Open Policy Agent) provides fine-grained, context-aware authorization decisions with a complete audit trail, without coupling policy logic to application code.

Why OPA for Agent Authorization

AI agents make runtime decisions about which tools to invoke, what data to access, and which models to call. Hardcoding authorization rules into the gateway or agent code creates a rigid, unauditable system. OPA separates policy from mechanism, enabling organizations to express security intent as inspectable, testable code.

  • Policy-as-code (Rego language). Authorization rules are written in Rego, a purpose-built language for policy decisions. Policies are version-controlled, peer-reviewed, and testable, following the same lifecycle as application code.
  • Decoupled from application code. The gateway queries OPA as an external decision point. Policy changes do not require gateway redeployment, code changes, or service restarts.
  • Fine-grained, context-aware decisions. OPA evaluates the full request context: caller SPIFFE ID, target tool, action type, session history, time of day, risk score, and any external data source.
  • Audit trail for every decision. Every policy evaluation produces a decision record with a unique ID, the input data, the policy bundle digest, and the result. Every decision is forensically reconstructible.
  • Hot-reload without gateway restart. Policy bundles are loaded and reloaded at runtime. New policies take effect within seconds without disrupting active connections or requiring gateway restarts.

How OPA Works in PRECINCT

At step 6 of the 13-layer middleware chain, the gateway sends the full request context to OPA for evaluation. OPA returns an allow/deny decision with an optional reason code and metadata.

flowchart LR A["Incoming
Request"] --> B["SPIFFE
Identity
Extraction"] B --> C["OPA Policy
Evaluation"] C --> D{Decision} D -->|"allow"| E["Continue to
Step 7+"] D -->|"deny"| F["403 Forbidden
+ Reason Code"] D -->|"step_up"| G["Require
Elevated Auth"] style A fill:#1e293b,stroke:#334155,color:#f1f5f9 style B fill:#1e293b,stroke:#334155,color:#f1f5f9 style C fill:#1e3a5f,stroke:#3b82f6,color:#f1f5f9 style D fill:#1e293b,stroke:#f59e0b,color:#f1f5f9 style E fill:#064e3b,stroke:#10b981,color:#f1f5f9 style F fill:#7f1d1d,stroke:#ef4444,color:#f1f5f9 style G fill:#78350f,stroke:#f59e0b,color:#f1f5f9

The gateway sends the following context to OPA with every evaluation request:

  • Caller SPIFFE ID (extracted from the mTLS SVID at step 3)
  • Requested tool name and registered capability ID
  • Action type (e.g., tools/call, resources/read)
  • Destination host and port
  • Session context: cumulative request count, risk score, prior tools invoked
  • Timestamp and request metadata

Policy Examples

The following Rego examples illustrate the kinds of policies PRECINCT supports. Each example addresses a different authorization concern.

1. Tool Allowlist by SPIFFE ID Pattern

Restrict which tools a given agent class can invoke based on its SPIFFE ID.

package precinct.authz

import rego.v1

# Allow researcher agents to use search and retrieval tools only
default allow := false

allow if {
    glob.match("spiffe://poc.local/agents/mcp-client/*/dev", [], input.caller_spiffe_id)
    input.tool_name in {"web_search", "arxiv_search", "document_retrieve"}
}

# Allow gateway-admin to use any tool
allow if {
    input.caller_spiffe_id == "spiffe://poc.local/gateways/mcp-security-gateway/dev"
}

2. Destination Restrictions

Prevent agents from contacting unauthorized external services.

package precinct.authz.destination

import rego.v1

# Approved external destinations
approved_destinations := {
    "api.groq.com",
    "api.openai.com",
    "api.anthropic.com",
}

default allow := false

allow if {
    input.destination_host in approved_destinations
}

deny_reason := "destination_not_approved" if {
    not input.destination_host in approved_destinations
}

3. Step-Up Requirements for High-Risk Tools

Require elevated authorization for tools classified as high-risk.

package precinct.authz.stepup

import rego.v1

high_risk_tools := {
    "execute_shell",
    "database_write",
    "file_delete",
    "send_email",
}

decision := "step_up" if {
    input.tool_name in high_risk_tools
    not input.step_up_verified
}

decision := "allow" if {
    input.tool_name in high_risk_tools
    input.step_up_verified
}

decision := "allow" if {
    not input.tool_name in high_risk_tools
}

4. Rate Limit Budget Allocation

Enforce per-agent request budgets within a session.

package precinct.authz.ratelimit

import rego.v1

# Maximum requests per session, by agent class
budget := {
    "researcher": 100,
    "analyst": 50,
    "monitor": 200,
}

default allow := false

allow if {
    agent_class := extract_class(input.caller_spiffe_id)
    max_requests := budget[agent_class]
    input.session_request_count < max_requests
}

deny_reason := "session_budget_exhausted" if {
    agent_class := extract_class(input.caller_spiffe_id)
    max_requests := budget[agent_class]
    input.session_request_count >= max_requests
}

extract_class(spiffe_id) := class if {
    parts := split(spiffe_id, "/")
    class := parts[4]
}

5. Model Provider Restrictions

Control which model providers each agent class is allowed to use.

package precinct.authz.models

import rego.v1

# Map agent classes to allowed model providers
allowed_providers := {
    "researcher": {"groq", "openai"},
    "analyst": {"anthropic", "openai"},
    "monitor": {"groq"},
}

default allow := false

allow if {
    agent_class := extract_class(input.caller_spiffe_id)
    providers := allowed_providers[agent_class]
    input.model_provider in providers
}

deny_reason := sprintf("agent class '%s' not authorized for provider '%s'",
    [extract_class(input.caller_spiffe_id), input.model_provider]) if {
    agent_class := extract_class(input.caller_spiffe_id)
    providers := allowed_providers[agent_class]
    not input.model_provider in providers
}

extract_class(spiffe_id) := class if {
    parts := split(spiffe_id, "/")
    class := parts[4]
}

Capability Registry V2

The Capability Registry is a YAML-based configuration that maps capability IDs to their adapter protocol, tool allowlists, authorization requirements, and step-up policies. OPA policies reference capability IDs rather than raw tool names, providing an abstraction layer that decouples policy from tool implementation details.

capabilities:
  - id: tool.default.mcp
    description: "Standard MCP tool invocation"
    adapter: mcp
    protocol: stdio
    tools:
      allow:
        - web_search
        - arxiv_search
        - document_retrieve
        - calculator
    authorization:
      action: "tools/call"
      resource: "mcp/*"
    step_up: false

  - id: tool.highrisk.cli
    description: "High-risk CLI tool execution"
    adapter: cli
    protocol: exec
    tools:
      allow:
        - execute_shell
        - file_write
        - database_query
    authorization:
      action: "tools/call"
      resource: "cli/*"
    step_up: true
    step_up_method: "human_approval"

  - id: model.groq.openai_compat
    description: "Groq inference via OpenAI-compatible API"
    adapter: openai_compat
    protocol: https
    endpoint: "https://api.groq.com/openai/v1"
    authorization:
      action: "model/inference"
      resource: "groq/*"
    step_up: false
    rate_limit:
      requests_per_minute: 30
      tokens_per_minute: 100000
Abstraction Layer

Capability IDs (tool.default.mcp, model.groq.openai_compat) decouple policy from implementation. When a tool migrates from MCP to a different adapter, only the registry entry changes; policies referencing the capability ID remain unchanged.

Policy Bundle Management

OPA policies are packaged as bundles and managed through a GitOps-driven lifecycle. Changes to policies follow the same rigor as changes to application code.

  • GitOps-driven policy updates. Policy files live in a Git repository. Merges to the main branch trigger bundle rebuilds and deployment to OPA instances. No manual policy editing in production.
  • Bundle digest logging. Every audit event includes the SHA-256 digest of the active policy bundle. This ensures forensic traceability: given any decision, you can reconstruct exactly which policies were in effect at the time.
  • Signed policy artifacts. Bundles are cryptographically signed before deployment. OPA verifies the signature on load, rejecting tampered or unsigned bundles.
  • Controlled change lifecycle. Policy changes go through pull request review, automated Rego unit tests (via opa test), staging deployment, and production rollout. Rollback is a Git revert.
# Build and sign a policy bundle
opa build -b policies/ -o bundle.tar.gz
cosign sign-blob --key cosign.key bundle.tar.gz

# Run Rego unit tests
opa test policies/ -v

# Deploy bundle to OPA (via bundle server or direct push)
opa run --server \
  --bundle bundle.tar.gz \
  --verification-key cosign.pub

Decision Audit Trail

Every OPA evaluation produces a structured decision record that enables full forensic reconstruction of any authorization decision. This is critical for compliance frameworks that require evidence of access control enforcement.

Each decision record includes:

  • Decision ID: A unique identifier for the evaluation (e.g., dec-4f8a2b1c-9d3e-4a7f-b5c6-1e2f3a4b5c6d).
  • Bundle digest: The SHA-256 hash of the policy bundle that was active at evaluation time.
  • SPIFFE identity: The full SPIFFE ID of the requesting workload.
  • Full input/output: The complete input context sent to OPA and the complete result, enabling forensic reconstruction of the decision logic.

Example Decision Record

{
  "decision_id": "dec-4f8a2b1c-9d3e-4a7f-b5c6-1e2f3a4b5c6d",
  "timestamp": "2026-02-21T14:32:07.842Z",
  "bundle_digest": "sha256:a1b2c3d4e5f6...",
  "result": "deny",
  "reason": "destination_not_approved",
  "input": {
    "caller_spiffe_id": "spiffe://poc.local/agents/mcp-client/dspy-researcher/dev",
    "tool_name": "http_request",
    "destination_host": "evil-exfiltration.example.com",
    "action": "tools/call",
    "session_id": "sess-abc123",
    "session_request_count": 14,
    "session_risk_score": 0.32
  },
  "metrics": {
    "evaluation_duration_ns": 142580,
    "policy_rules_evaluated": 7
  }
}
Compliance Alignment

The decision audit trail directly satisfies control requirements in SOC 2 (CC6.1, Logical Access), ISO 27001 (A.9.4, System and Application Access Control), and HIPAA (164.312(a), Access Control). Auditors can query the decision log by SPIFFE ID, time range, decision result, or bundle version to produce compliance evidence.