A ready-to-run Open Policy Agent (OPA) server with sample Rego policies designed as guardrails for the TrueFoundry AI Gateway.
- Overview
- How It Works
- Policies Included
- Prerequisites
- Quick Start
- Policy Reference
- Input Schema
- Configuring TrueFoundry AI Gateway
- Testing Your Policies
- Writing Custom Policies
- Production Considerations
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.
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.
| 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 |
| 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.
git clone <this-repo-url>
cd sample-opa-server
docker compose up --buildThe 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"}# 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/healthTo 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# 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 ./policiesEndpoint: POST /v1/data/authz_req/allow
The most comprehensive input-side policy. It enforces:
- Model allowlist — only models in
allowed_modelsare permitted. - Model blocklist — models in
blocked_modelsare always denied, even if on the allowlist. - Keyword filtering — user messages are scanned for prompt-injection patterns (
jailbreak,bypass security, etc.). - Harmful content — user messages are scanned for dangerous topics.
- 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_lengthat the top of the file.
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.
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.
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
}Endpoint: POST /v1/data/authz_team_deny/allow
Blocks users from other-team while allowing everyone else. Uses a blocklist (default allow) pattern.
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.
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.
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.
Endpoint: POST /v1/data/mcp_output/result
Note: this policy exposes
result(notallow) 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.
The TrueFoundry AI Gateway sends a JSON body to OPA wrapped under an input key. The structure varies slightly by hook type:
{
"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
}
}
}{
"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"
}
}
}{
"input": {
"request": {
"content": [
{ "text": "Here is information about microsoft/vscode..." }
]
},
"metadata": {
"mcp_server_name": "deepwiki-sucecsui"
},
"context": {
"hook_type": "output"
}
}
}| 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 |
After your OPA server is running (e.g. at http://opa-server:8181), configure a guardrail in TrueFoundry:
- Go to AI Gateway → Guardrails in the TrueFoundry dashboard.
- Click Add Guardrail → OPA.
- 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) |
- Attach the guardrail to a Gateway Route or Provider in your AI Gateway config.
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_closedTip: Start with
mode: auditto observe traffic without blocking. Switch tomode: enforceonce you've confirmed the policy behaves correctly.
All examples assume the server is running on http://localhost:8181.
# 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 } }# 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 } }# 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..." } }# 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 } }# 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'" } }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 .- Create a new
.regofile inpolicies/. - Choose a unique package name — this becomes part of your OPA endpoint path.
- Implement your logic. Return
allow := {"allow": <bool>}(orresult := {"allow": <bool>}if you want the top-level rule to beresult).
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}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.).
# 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 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/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=3The 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.
| 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.
- 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 \
/policiesInstead 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.gzsample-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
- TrueFoundry OPA Guardrails Documentation
- Open Policy Agent Documentation
- Rego Language Reference
- OPA Playground — test Rego policies in the browser