Proxy Configurations
Copy-paste- Application Layer: Python SDK (@guard decorator)
- Network Layer: Envoy Proxy (Sidecar)
- Infrastructure Layer: Kubernetes Network Policies
Application Layer (@guard)
The @guard decorator validates requests at the application layer.
For guidance on which pattern to use, see Kubernetes Integration.
Envoy External Authorization
Tenuo integrates with Envoy as an external authorization service via ext_authz.
Architecture
┌─────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────┐
│ Client │────▶│ Envoy │────▶│ Tenuo Authz │ │ Backend │
│ │ │ │ │ (9090) │ │ │
└─────────┘ │ │◀────│ 200 or 403 │ │ │
│ │ └─────────────┘ │ │
│ │────────────────────────▶│ │
│ │ (only if 200) │ │
└─────────┘ └─────────┘
Full Envoy Config
# envoy.yaml
static_resources:
listeners:
- name: main
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress
route_config:
name: local_route
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: backend }
http_filters:
# Tenuo authorization filter
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
grpc_service:
envoy_grpc:
cluster_name: tenuo-authorizer
timeout: 0.25s
include_peer_certificate: true
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: tenuo-authorizer
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {} # Required for gRPC
load_assignment:
cluster_name: tenuo-authorizer
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: tenuo-authorizer
port_value: 9090
- name: backend
connect_timeout: 0.5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: backend
port_value: 8080
HTTP Mode (Alternative)
If you prefer HTTP over gRPC:
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
http_service:
server_uri:
uri: http://tenuo-authorizer:9090
cluster: tenuo-authorizer
timeout: 0.25s
authorization_request:
allowed_headers:
patterns:
- exact: x-tenuo-warrant
- exact: x-tenuo-pop
- exact: content-type
authorization_response:
allowed_upstream_headers:
patterns:
- exact: x-tenuo-warrant-id
Istio Integration
Step 1: Configure ExtensionProvider
Add Tenuo as an external authorization provider in Istio’s mesh config:
# istio-config.yaml
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
meshConfig:
extensionProviders:
- name: tenuo-ext-authz
envoyExtAuthzGrpc:
service: tenuo-authorizer.tenuo-system.svc.cluster.local
port: 9090
Or patch an existing installation:
kubectl edit configmap istio -n istio-system
data:
mesh: |
extensionProviders:
- name: tenuo-ext-authz
envoyExtAuthzGrpc:
service: tenuo-authorizer.tenuo-system.svc.cluster.local
port: 9090
Step 2: Apply AuthorizationPolicy
# authz-policy.yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: tenuo-authz
namespace: default
spec:
selector:
matchLabels:
app: my-agent # Apply to pods with this label
action: CUSTOM
provider:
name: tenuo-ext-authz
rules:
- to:
- operation:
paths: ["/api/*"] # Paths requiring authorization
Selective Enforcement
Apply to specific methods:
rules:
- to:
- operation:
methods: ["POST", "PUT", "DELETE"]
paths: ["/api/tools/*"]
Exclude health checks:
rules:
- to:
- operation:
notPaths: ["/health", "/ready", "/metrics"]
nginx Integration
Use the auth_request directive:
# nginx.conf
upstream backend {
server localhost:8080;
}
upstream tenuo {
server localhost:9090;
}
server {
listen 80;
# Internal auth endpoint
location = /_tenuo_auth {
internal;
proxy_pass http://tenuo/authorize;
proxy_pass_request_body on;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Tenuo-Warrant $http_x_tenuo_warrant;
proxy_set_header X-Tenuo-PoP $http_x_tenuo_pop;
}
# Protected routes
location /api/ {
auth_request /_tenuo_auth;
error_page 401 403 = @denied;
proxy_pass http://backend;
}
location @denied {
return 403 '{"error": "authorization_denied"}';
add_header Content-Type application/json;
}
# Unprotected routes
location /health {
proxy_pass http://backend;
}
}
Control Plane Fetch
Full implementation for fetching warrants per-task.
Agent Code
import os
import httpx
from tenuo import Warrant, SigningKey, warrant_scope, key_scope
# Load keypair once at startup
keypair = SigningKey.from_pem(os.getenv("TENUO_KEYPAIR_PEM"))
def get_k8s_token() -> str:
"""Read the pod's service account token."""
with open("/var/run/secrets/kubernetes.io/serviceaccount/token") as f:
return f.read()
async def get_warrant(
tools: list[str],
constraints: dict,
ttl: int = 60
) -> Warrant:
"""Fetch a task-scoped warrant from the control plane."""
async with httpx.AsyncClient() as client:
resp = await client.post(
"http://control-plane.tenuo-system.svc.cluster.local:8080/v1/warrants",
headers={"Authorization": f"Bearer {get_k8s_token()}"},
json={
"tools": tools,
"constraints": constraints,
"ttl_seconds": ttl,
"holder": keypair.public_key.to_hex(),
},
timeout=5.0
)
resp.raise_for_status()
return Warrant.from_base64(resp.json()["warrant"])
async def handle_task(user_request: str):
"""Example: handle a task with scoped authority."""
# Fetch warrant scoped to this specific task
warrant = await get_warrant(
tools=["read_file", "search"],
constraints={"path": "/data/reports/*"},
ttl=60
)
with warrant_scope(warrant), key_scope(keypair):
result = await agent.invoke({"input": user_request})
# Warrant expires automatically — no cleanup needed
return result
Kubernetes Manifests
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: agent-keypair
type: Opaque
stringData:
KEYPAIR_PEM: |
-----BEGIN PRIVATE KEY-----
... generate with: tenuo keygen --format pem ...
-----END PRIVATE KEY-----
---
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: agent
spec:
template:
spec:
serviceAccountName: agent-sa # For control plane auth
containers:
- name: agent
image: your-agent:latest
env:
- name: TENUO_KEYPAIR_PEM
valueFrom:
secretKeyRef:
name: agent-keypair
key: KEYPAIR_PEM
> [!CAUTION]
> **Production Safety**: Do NOT set `TENUO_ENV="test"` in your production manifests.
> This variable enables development-only bypass modes that disable authorization checks.
Request Header
Warrant passed via HTTP header, validated in middleware.
from fastapi import FastAPI, Request, HTTPException
from tenuo import Warrant, SigningKey, warrant_scope, key_scope
import os
app = FastAPI()
# Load keypair once at startup
keypair = SigningKey.from_pem(os.getenv("TENUO_KEYPAIR_PEM"))
@app.middleware("http")
async def tenuo_middleware(request: Request, call_next):
# Skip unprotected paths
if request.url.path in ["/health", "/ready"]:
return await call_next(request)
# Require warrant header
warrant_b64 = request.headers.get("X-Tenuo-Warrant")
if not warrant_b64:
raise HTTPException(401, "Missing X-Tenuo-Warrant header")
try:
warrant = Warrant.from_base64(warrant_b64)
except Exception as e:
raise HTTPException(400, f"Invalid warrant: {e}")
if warrant.is_expired():
raise HTTPException(401, "Warrant expired")
with warrant_scope(warrant), key_scope(keypair):
return await call_next(request)
Environment Variable
Warrant loaded from Secret at pod startup.
Kubernetes Manifests
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: tenuo-credentials
type: Opaque
stringData:
WARRANT_BASE64: |
eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9...
KEYPAIR_PEM: |
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
---
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: agent
spec:
template:
spec:
containers:
- name: agent
image: your-agent:latest
env:
- name: TENUO_WARRANT_BASE64
valueFrom:
secretKeyRef:
name: tenuo-credentials
key: WARRANT_BASE64
- name: TENUO_KEYPAIR_PEM
valueFrom:
secretKeyRef:
name: tenuo-credentials
key: KEYPAIR_PEM
Agent Code
import os
from tenuo import Warrant, SigningKey, warrant_scope, key_scope
# Load once at startup
warrant = Warrant.from_base64(os.getenv("TENUO_WARRANT_BASE64"))
keypair = SigningKey.from_pem(os.getenv("TENUO_KEYPAIR_PEM"))
def run_agent(prompt: str):
with warrant_scope(warrant), key_scope(keypair):
return agent.invoke({"input": prompt})
Docker Compose (Local Development)
# docker-compose.yaml
version: '3.8'
services:
agent:
build: .
environment:
- TENUO_KEYPAIR_PEM=${TENUO_KEYPAIR_PEM}
depends_on:
- tenuo-authorizer
tenuo-authorizer:
image: tenuo/authorizer:0.1
ports:
- "9090:9090"
environment:
- TRUSTED_ISSUERS=${CONTROL_PLANE_PUBLIC_KEY}
volumes:
- ./gateway.yaml:/etc/tenuo/gateway.yaml:ro
control-plane:
image: tenuo/demo-control-plane:0.1
ports:
- "8080:8080"
environment:
- SIGNING_KEY=${CONTROL_PLANE_PRIVATE_KEY}
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- agent
- tenuo-authorizer
.env file
# Generate with: tenuo keygen
CONTROL_PLANE_PRIVATE_KEY=...
CONTROL_PLANE_PUBLIC_KEY=...
TENUO_KEYPAIR_PEM="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
Direct SDK (No Proxy)
For simple deployments without a sidecar or gateway.
from fastapi import FastAPI, Request, HTTPException
from tenuo import (
Authorizer, Warrant, SigningKey, PublicKey,
warrant_scope, key_scope, guard
)
import os
app = FastAPI()
# Initialize authorizer with trusted root
control_plane_key = PublicKey.from_hex(os.getenv("TRUSTED_ISSUER_KEY"))
authorizer = Authorizer(trusted_roots=[control_plane_key])
# Load service keypair
keypair = SigningKey.from_pem(os.getenv("TENUO_KEYPAIR_PEM"))
@app.middleware("http")
async def tenuo_middleware(request: Request, call_next):
warrant_b64 = request.headers.get("X-Tenuo-Warrant")
if not warrant_b64:
raise HTTPException(401, "Missing warrant")
try:
warrant = Warrant.from_base64(warrant_b64)
authorizer.verify(warrant) # Checks signature + expiry
except Exception as e:
raise HTTPException(403, f"Authorization failed: {e}")
with warrant_scope(warrant), key_scope(keypair):
return await call_next(request)
@app.post("/api/files/read")
@guard(tool="read_file")
async def read_file(path: str):
# @guard checks: tool in warrant, path matches constraints
return {"content": open(path).read()}
@app.post("/api/files/write")
@guard(tool="write_file")
async def write_file(path: str, content: str):
open(path, "w").write(content)
return {"status": "ok"}
Gateway Configuration
See Gateway Configuration Reference for the full YAML schema.
See Also
- Kubernetes Integration - Patterns, decisions, debugging
- Envoy Quickstart
- Istio Quickstart
- API Reference