Skip to content

truefoundry/sample-opa-server

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sample OPA Server — TrueFoundry AI Gateway Guardrails

A ready-to-run Open Policy Agent (OPA) server with sample Rego policies designed as guardrails for the TrueFoundry AI Gateway.


Table of Contents


Overview

TrueFoundry AI Gateway supports OPA-based guardrails that evaluate authorization decisions across the full LLM lifecycle:

  • LLM Input — validate requests before they reach the model (model allowlists, prompt injection detection, user/team identity checks)
  • LLM Output — filter responses before they reach the user (content filtering, PII detection)
  • MCP Pre-Tool — control which MCP tools can be invoked and with what arguments

This repo gives you a working OPA server with nine ready-made policies covering the most common guardrail patterns. Clone it, run it, point TrueFoundry at it, and customise from there.


How It Works

User Request
     │
     ▼
┌─────────────────────────────────┐
│     TrueFoundry AI Gateway      │
│                                 │
│  1. Calls OPA with input JSON   │──► POST /v1/data/<policy>/allow
│                                 │         │
│  2. OPA evaluates Rego policy   │◄── { "result": { "allow": true } }
│                                 │
│  3a. allow=true  → forward to LLM Provider
│  3b. allow=false → return 403 to user
└─────────────────────────────────┘
     │ (if allowed)
     ▼
LLM Provider (OpenAI / Vertex / Anthropic / …)
     │
     ▼
TrueFoundry AI Gateway (optional output check via OPA)
     │
     ▼
User Response

The gateway sends a POST request to your OPA server containing the full context (request body, user identity, tool metadata). OPA evaluates your Rego policy and returns {"result": {"allow": true|false}}. A false blocks the request with an error response to the user.


Policies Included

File Package OPA Endpoint Hook Type Description
policy_req.rego authz_req /v1/data/authz_req/allow LLM Input Model allowlist/blocklist, blocked keywords, harmful content, message length cap
policy_bearer.rego authz_bearer /v1/data/authz_bearer/allow LLM Input Allow only users from a trusted email domain
policy_model_access.rego authz_model_access /v1/data/authz_model_access/allow LLM Input Restrict sensitive models to a trusted email domain
authz_team_allow.rego authz_team_allow /v1/data/authz_team_allow/allow LLM Input Allow only users from a specific team
authz_team_deny.rego authz_team_deny /v1/data/authz_team_deny/allow LLM Input Block users from a specific team
policy_mcp.rego authz_mcp /v1/data/authz_mcp/allow MCP Pre-Tool Block specific MCP tools and/or server names
mcp_fetch.rego authz_fetch /v1/data/authz_fetch/allow MCP Pre-Tool Block the fetch tool from a specific MCP server
policy_args.rego authz_args /v1/data/authz_args/allow MCP Pre-Tool Block tool calls whose url argument is on a blocklist
output_tool.rego mcp_output /v1/data/mcp_output/result LLM Output / MCP Post-Tool Filter responses that contain disallowed content

Prerequisites

Tool Version Install
Docker 20.10+ docs.docker.com
Docker Compose v2+ Bundled with Docker Desktop
curl + jq any For running the test examples

No other dependencies are required. The Docker image (openpolicyagent/opa:latest) is pulled automatically.


Quick Start

Option 1 — Docker Compose (recommended)

git clone <this-repo-url>
cd sample-opa-server

docker compose up --build

The OPA server starts on http://localhost:8181.

The docker-compose.yml mounts ./policies into the container and runs OPA with --watch, so any change you save to a .rego file is picked up immediately without restarting.

Verify it is running:

curl http://localhost:8181/health
# {"status":"ok"}

Option 2 — Docker (manual)

# Build the image
docker build -t sample-opa-server .

# Run the container
docker run -d \
  --name opa-server \
  -p 8181:8181 \
  sample-opa-server

# Verify
curl http://localhost:8181/health

To edit policies without rebuilding, mount the directory:

docker run -d \
  --name opa-server \
  -p 8181:8181 \
  -v "$(pwd)/policies:/policies" \
  openpolicyagent/opa:latest \
  run --server --addr :8181 --log-level info --watch /policies

Option 3 — OPA binary (no Docker)

# macOS
brew install opa

# or download directly
curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_darwin_arm64
chmod +x opa

# Run
./opa run --server --addr :8181 --log-level info --watch ./policies

Policy Reference

policy_req.rego — Request Validation

Endpoint: POST /v1/data/authz_req/allow

The most comprehensive input-side policy. It enforces:

  1. Model allowlist — only models in allowed_models are permitted.
  2. Model blocklist — models in blocked_models are always denied, even if on the allowlist.
  3. Keyword filtering — user messages are scanned for prompt-injection patterns (jailbreak, bypass security, etc.).
  4. Harmful content — user messages are scanned for dangerous topics.
  5. Message length — total character count across all messages must not exceed max_total_message_length (default 500,000).

Customise:

  • Edit allowed_models, blocked_models, blocked_keywords, harmful_patterns, max_total_message_length at the top of the file.

policy_bearer.rego — Email Domain Authorization

Endpoint: POST /v1/data/authz_bearer/allow

Allows requests only from users whose user_email ends with a trusted domain.

trusted_domain := "@truefoundry.com"

Customise: Change trusted_domain to your organisation's domain.


policy_model_access.rego — Sensitive Model Gate

Endpoint: POST /v1/data/authz_model_access/allow

Two-tier access:

  • Non-sensitive models → allowed for everyone.
  • Sensitive models (e.g. gpt-4, gemini-2-5-flash) → allowed only for users with a trusted email domain.

Returns a desc field explaining the decision, which TrueFoundry can surface to the user.


authz_team_allow.rego — Team Allowlist

Endpoint: POST /v1/data/authz_team_allow/allow

Permits access only to users who are members of test-team. Uses an allowlist (default deny) pattern.

Customise: Replace "test-team" with your team name. You can also extend to multiple teams:

allow_decision if {
    allowed_teams := {"eng-team", "ml-team"}
    team := input.metadata.subject.teamName[_]
    team in allowed_teams
}

authz_team_deny.rego — Team Blocklist

Endpoint: POST /v1/data/authz_team_deny/allow

Blocks users from other-team while allowing everyone else. Uses a blocklist (default allow) pattern.


policy_mcp.rego — MCP Tool & Server Blocking

Endpoint: POST /v1/data/authz_mcp/allow

Uses a blocklist (default allow) approach for MCP pre-tool hooks:

  • blocked_tools — specific tool names to deny (e.g. multiply, divide).
  • blocked_server_names — deny all tools from specific MCP servers.

mcp_fetch.rego — Targeted Fetch Tool Block

Endpoint: POST /v1/data/authz_fetch/allow

Blocks the fetch tool only when it is called from the nm-fetch MCP server. The same tool on any other server is allowed. Demonstrates per-(server, tool) granularity.


policy_args.rego — Tool Argument URL Filtering

Endpoint: POST /v1/data/authz_args/allow

Inspects tool call arguments for a url key. If the URL is in blocked_urls, the tool call is denied.

Customise: Add URLs to the blocked_urls list.


output_tool.rego — Output / Response Filtering

Endpoint: POST /v1/data/mcp_output/result

Note: this policy exposes result (not allow) at the rule level, so the OPA path ends in /result.

Scans LLM or MCP tool responses for disallowed content. The example blocks any response from the deepwiki-sucecsui server that mentions microsoft/vscode.


Input Schema

The TrueFoundry AI Gateway sends a JSON body to OPA wrapped under an input key. The structure varies slightly by hook type:

LLM Input hook

{
  "input": {
    "request": {
      "model": "openai/gpt-4o-mini",
      "messages": [
        { "role": "system", "content": "You are a helpful assistant." },
        { "role": "user",   "content": "What is the capital of France?" }
      ]
    },
    "metadata": {
      "user_email": "alice@truefoundry.com",
      "subject": {
        "teamName": ["engineering", "ml-platform"]
      }
    },
    "context": {
      "hook_type": "input",
      "streaming": false
    }
  }
}

MCP Pre-Tool hook

{
  "input": {
    "request": {
      "arguments": {
        "url": "https://example.com/page"
      }
    },
    "metadata": {
      "user_email": "alice@truefoundry.com",
      "tool_name": "fetch",
      "mcp_server_name": "nm-fetch"
    },
    "context": {
      "hook_type": "mcp_pre_tool"
    }
  }
}

LLM Output / MCP Post-Tool hook

{
  "input": {
    "request": {
      "content": [
        { "text": "Here is information about microsoft/vscode..." }
      ]
    },
    "metadata": {
      "mcp_server_name": "deepwiki-sucecsui"
    },
    "context": {
      "hook_type": "output"
    }
  }
}

Key fields

Field Type Description
input.request.model string The LLM model identifier
input.request.messages array Chat messages (role + content)
input.request.arguments object Tool call arguments (MCP)
input.request.content array Response content objects (output hook)
input.metadata.user_email string Authenticated user's email
input.metadata.subject.teamName array User's team memberships
input.metadata.tool_name string MCP tool being called
input.metadata.mcp_server_name string MCP server name
input.context.hook_type string "input", "output", or "mcp_pre_tool"
input.context.streaming bool Whether the request is a streaming call

Configuring TrueFoundry AI Gateway

After your OPA server is running (e.g. at http://opa-server:8181), configure a guardrail in TrueFoundry:

  1. Go to AI Gateway → Guardrails in the TrueFoundry dashboard.
  2. Click Add Guardrail → OPA.
  3. Fill in the configuration:
Field Value
OPA Server URL http://<your-opa-host>:8181
Policy Path e.g. /v1/data/authz_req/allow
Hook Type input, output, or mcp_pre_tool
Enforcement Mode enforce (block on deny) or audit (log only)
On Error fail_open (allow on OPA error) or fail_closed (block on OPA error)
  1. Attach the guardrail to a Gateway Route or Provider in your AI Gateway config.

Example: Protecting a route with email-domain auth

routes:
  - name: my-llm-route
    guardrails:
      - type: opa
        url: http://opa-server:8181
        path: /v1/data/authz_bearer/allow
        hook: input
        mode: enforce
        on_error: fail_closed

Tip: Start with mode: audit to observe traffic without blocking. Switch to mode: enforce once you've confirmed the policy behaves correctly.


Testing Your Policies

All examples assume the server is running on http://localhost:8181.

Bearer / Email Auth

# Should allow (trusted domain)
curl -s -X POST http://localhost:8181/v1/data/authz_bearer/allow \
  -H "Content-Type: application/json" \
  -d @examples/inputs/bearer_allowed.json | jq .
# { "result": { "allow": true } }

# Should deny (untrusted domain)
curl -s -X POST http://localhost:8181/v1/data/authz_bearer/allow \
  -H "Content-Type: application/json" \
  -d @examples/inputs/bearer_denied.json | jq .
# { "result": { "allow": false } }

Request Validation

# Should allow (safe message, allowed model)
curl -s -X POST http://localhost:8181/v1/data/authz_req/allow \
  -H "Content-Type: application/json" \
  -d @examples/inputs/req_allowed.json | jq .
# { "result": { "allow": true } }

# Should deny (blocked keyword: "jailbreak")
curl -s -X POST http://localhost:8181/v1/data/authz_req/allow \
  -H "Content-Type: application/json" \
  -d @examples/inputs/req_denied_blocked_keyword.json | jq .
# { "result": { "allow": false } }

# Should deny (blocked model: "openai-main/gpt-4")
curl -s -X POST http://localhost:8181/v1/data/authz_req/allow \
  -H "Content-Type: application/json" \
  -d @examples/inputs/req_denied_blocked_model.json | jq .
# { "result": { "allow": false } }

Model Access Control

# Should allow (sensitive model, trusted email)
curl -s -X POST http://localhost:8181/v1/data/authz_model_access/allow \
  -H "Content-Type: application/json" \
  -d @examples/inputs/model_access_allowed.json | jq .
# { "result": { "allow": true, "desc": "Request allowed" } }

# Should deny (sensitive model, untrusted email)
curl -s -X POST http://localhost:8181/v1/data/authz_model_access/allow \
  -H "Content-Type: application/json" \
  -d @examples/inputs/model_access_denied.json | jq .
# { "result": { "allow": false, "desc": "Request blocked: your email domain is not permitted..." } }

Team-Based Access

# Should allow (user is in "test-team")
curl -s -X POST http://localhost:8181/v1/data/authz_team_allow/allow \
  -H "Content-Type: application/json" \
  -d @examples/inputs/team_allow_allowed.json | jq .
# { "result": { "allow": true } }

# Should deny (user is not in "test-team")
curl -s -X POST http://localhost:8181/v1/data/authz_team_allow/allow \
  -H "Content-Type: application/json" \
  -d @examples/inputs/team_allow_denied.json | jq .
# { "result": { "allow": false } }

MCP Tool Blocking

# Should deny (blocked tool + blocked server)
curl -s -X POST http://localhost:8181/v1/data/authz_mcp/allow \
  -H "Content-Type: application/json" \
  -d @examples/inputs/mcp_tool_denied.json | jq .
# { "result": { "allow": false, "description": "Tool 'multiply' is blocked" } }

# Should allow (safe tool + safe server)
curl -s -X POST http://localhost:8181/v1/data/authz_mcp/allow \
  -H "Content-Type: application/json" \
  -d @examples/inputs/mcp_tool_allowed.json | jq .
# { "result": { "allow": true, "description": "Tool 'search' is allowed on server 'my-search-server'" } }

Inline curl (no file needed)

curl -s -X POST http://localhost:8181/v1/data/authz_bearer/allow \
  -H "Content-Type: application/json" \
  -d '{"input": {"metadata": {"user_email": "user@truefoundry.com"}}}' | jq .

Writing Custom Policies

File structure

  1. Create a new .rego file in policies/.
  2. Choose a unique package name — this becomes part of your OPA endpoint path.
  3. Implement your logic. Return allow := {"allow": <bool>} (or result := {"allow": <bool>} if you want the top-level rule to be result).

Minimal template

package my_custom_policy

# OPA endpoint: POST /v1/data/my_custom_policy/allow

# Default deny — be explicit about what you allow
default allow_decision := false

allow_decision if {
    # your conditions here
    input.metadata.user_email == "admin@example.com"
}

# Return format expected by TrueFoundry AI Gateway
allow := {"allow": allow_decision}

OPA endpoint derivation

The URL path is derived from the package name:

Package Endpoint
package foo /v1/data/foo/<rule_name>
package foo.bar /v1/data/foo/bar/<rule_name>
package authz_req /v1/data/authz_req/allow

The <rule_name> is whatever you name your top-level rule (allow, result, etc.).

Testing a policy with opa eval

# Evaluate a policy locally without running a server
opa eval \
  --data policies/policy_bearer.rego \
  --input examples/inputs/bearer_allowed.json \
  'data.authz_bearer.allow'

OPA unit tests

OPA has a built-in test runner. Create a file ending in _test.rego:

package authz_bearer_test

import data.authz_bearer

test_trusted_email_allowed if {
    authz_bearer.allow_decision with input as {
        "metadata": {"user_email": "user@truefoundry.com"}
    }
}

test_untrusted_email_denied if {
    not authz_bearer.allow_decision with input as {
        "metadata": {"user_email": "user@gmail.com"}
    }
}

Run tests:

opa test policies/

Production Considerations

High availability

OPA is stateless — run multiple replicas behind a load balancer. All replicas load the same policy bundle at startup.

# Scale with Docker Compose
docker compose up --scale opa=3

Timeouts

The TrueFoundry gateway enforces a timeout on OPA calls. Keep policies lightweight — avoid large in-memory data lookups in the hot path. For data-heavy policies (e.g. allowlists with thousands of entries), load data via OPA bundles rather than hardcoding arrays.

Enforcement modes

Mode Behaviour on deny Behaviour on OPA error
enforce + fail_open Block request Allow request
enforce + fail_closed Block request Block request
audit Allow + log violation Allow + log error

Recommended: start with audit mode, validate, then switch to enforce + fail_closed for production.

Observability

  • OPA exposes Prometheus metrics at http://localhost:8181/metrics
  • Decision logs can be shipped to a remote bundle server or stdout (parsed by your log aggregator)

Enable decision logging:

opa run \
  --server \
  --addr :8181 \
  --log-level info \
  --set decision_logs.console=true \
  /policies

Policy bundles (for dynamic updates)

Instead of baking policies into the Docker image, use OPA's bundle feature to fetch policies from an HTTP server or object storage. This allows zero-downtime policy updates:

opa run \
  --server \
  --addr :8181 \
  --bundle https://your-bundle-server/policies.tar.gz

Repository Structure

sample-opa-server/
├── Dockerfile               # Builds the OPA server image
├── docker-compose.yml       # One-command start with hot-reload
├── policies/                # All Rego policy files
│   ├── authz_team_allow.rego
│   ├── authz_team_deny.rego
│   ├── mcp_fetch.rego
│   ├── output_tool.rego
│   ├── policy_args.rego
│   ├── policy_bearer.rego
│   ├── policy_mcp.rego
│   ├── policy_model_access.rego
│   └── policy_req.rego
└── examples/
    └── inputs/              # Sample JSON payloads for curl testing
        ├── bearer_allowed.json
        ├── bearer_denied.json
        ├── mcp_tool_allowed.json
        ├── mcp_tool_denied.json
        ├── model_access_allowed.json
        ├── model_access_denied.json
        ├── req_allowed.json
        ├── req_denied_blocked_keyword.json
        ├── req_denied_blocked_model.json
        ├── team_allow_allowed.json
        └── team_allow_denied.json

References

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors