Kubernetes Integration

This guide covers how to deploy and operate Tenuo in Kubernetes.


Architecture Overview

┌─────────────────────────────────────────────────────────────────────────┐
│                           YOUR CLUSTER                                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ┌──────────────┐      ┌──────────────────────────────────────────┐    │
│   │              │      │              AGENT POD                    │    │
│   │   CONTROL    │      │  ┌─────────────────┐  ┌───────────────┐  │    │
│   │    PLANE     │◄────►│  │ tenuo-authorizer│  │  Your Agent   │  │    │
│   │              │      │  │   (sidecar)     │◄─┤               │  │    │
│   │ • Issues     │      │  │                 │  │ • LangChain   │  │    │
│   │   warrants   │      │  │ • Verifies      │  │ • LangGraph   │  │    │
│   │ • Holds root │      │  │   warrants      │  │ • Your code   │  │    │
│   │   key        │      │  │ • Checks PoP    │  │               │  │    │
│   │              │      │  │ • ~27μs         │  │ • Holds       │  │    │
│   │  (you build  │      │  │                 │  │   keypair     │  │    │
│   │   this)      │      │  │(tenuo/authorizer)│ │               │  │    │
│   └──────────────┘      │  └─────────────────┘  └───────────────┘  │    │
│                         └──────────────────────────────────────────┘    │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Components

Component What It Does Who Provides It
Control Plane Issues warrants, holds root signing key You build this
tenuo-authorizer Verifies warrants, checks PoP, returns allow/deny Tenuo (tenuo/authorizer image)
Agent Executes tasks, holds keypair for PoP signatures You build this

Authorizer Deployment Options

Option Description Best For
Sidecar Per-pod container Fine-grained control
Gateway Cluster-wide via Envoy/Istio Centralized policy
SDK only No separate container Simple deployments

Quickstarts

Get a working setup in 5 minutes:


Helm Chart

For production deployments, use the official Helm chart:

helm install tenuo-authorizer ./charts/tenuo-authorizer \
  --namespace tenuo-system --create-namespace \
  --set config.trustedRoots[0]="YOUR_CONTROL_PLANE_PUBLIC_KEY"

The chart includes:

  • High Availability: Pod anti-affinity, PodDisruptionBudget
  • Autoscaling: HPA support with sensible defaults
  • Security: Non-root, read-only filesystem, minimal capabilities
  • Gateway Config: Full tool/route extraction via ConfigMap

See charts/tenuo-authorizer/README.md for full configuration options.


Choosing a Pattern

Pattern Warrant Scope Complexity Best For
Control Plane Fetch Per-task Medium Task-scoped authority
Request Header Per-request Medium Ingress injects warrants
Environment Variable Per-pod Low Batch jobs, prototyping

Decision Flowchart

Is warrant scope static for the pod lifetime?
├── Yes → Environment Variable (simple, but loses task-scoping)
└── No  → Does your ingress/mesh inject warrants?
          ├── Yes → Request Header
          └── No  → Control Plane Fetch (recommended)

Fetch a warrant from your control plane when each task starts. This is the canonical pattern that Tenuo was designed for.

When to use: Production systems where you want task-scoped, short-lived authority.

async def handle_task(user_request: str):
    # Fetch warrant scoped to this task
    warrant = await control_plane.get_warrant(
        tools=["read_file"],
        constraints={"path": "/data/reports/*"},
        ttl=60
    )
    
    with warrant_scope(warrant), key_scope(keypair):
        result = await agent.invoke(user_request)
    
    # Warrant expires, no cleanup needed

Full code: Enforcement: Proxy Configurations


Pattern: Request Header

Warrant passed per-request via X-Tenuo-Warrant header. Your ingress or mesh injects the warrant.

When to use: You have infrastructure that can inject warrants (API gateway, service mesh).

@app.middleware("http")
async def tenuo_middleware(request: Request, call_next):
    warrant = Warrant.from_base64(request.headers["X-Tenuo-Warrant"])
    with warrant_scope(warrant), key_scope(keypair):
        return await call_next(request)

Full code: Enforcement: Proxy Configurations


Pattern: Environment Variable

Warrant loaded at pod startup from a Secret.

When to use: Batch jobs, static workloads, or initial prototyping.

env:
- name: TENUO_WARRANT_BASE64
  valueFrom:
    secretKeyRef:
      name: tenuo-credentials
      key: WARRANT_BASE64

Full code: Enforcement: Proxy Configurations

Anti-Pattern Warning

Long-lived warrants in environment variables defeat Tenuo’s purpose:

# 24-hour warrant in env var = IAM with extra steps
env:
  - name: TENUO_WARRANT_BASE64
    value: "eyJ..."  # TTL: 86400s

# Short-lived, per-task warrants from control plane

Deploying the Authorizer

As Sidecar

containers:
- name: tenuo-authorizer
  image: tenuo/authorizer:0.1
  args: ["serve", "--config", "/etc/tenuo/gateway.yaml"]
  ports:
  - name: http
    containerPort: 9090
  env:
  - name: TENUO_TRUSTED_KEYS
    value: "<control-plane-public-key-hex>"
  resources:
    requests: { memory: "32Mi", cpu: "10m" }
    limits: { memory: "64Mi", cpu: "100m" }
  livenessProbe:
    httpGet:
      path: /health
      port: 9090
    initialDelaySeconds: 5
    periodSeconds: 10
  readinessProbe:
    httpGet:
      path: /ready
      port: 9090
    initialDelaySeconds: 3
    periodSeconds: 5
- name: agent
  # Your agent container

The authorizer binary reads trusted keys from TENUO_TRUSTED_KEYS (comma-separated hex). When using the Helm chart, keys are set via config.trustedRoots in values.yaml instead.

As Gateway

See Proxy Configurations for Envoy and Istio configs.


Key Rotation

Rotate signing keys without downtime.

Steps

  1. Add new key to trusted keys ```yaml env:
    • name: TENUO_TRUSTED_KEYS value: “OLD_KEY_HEX,NEW_KEY_HEX”
      Or, if using the Helm chart:
      ```bash
      helm upgrade tenuo-authorizer ./charts/tenuo-authorizer \
      --set config.trustedRoots[0]="OLD_KEY_HEX" \
      --set config.trustedRoots[1]="NEW_KEY_HEX"
      
  2. Roll out authorizer
    kubectl rollout restart deployment/tenuo-authorizer
    
  3. Update control plane to sign with new key

  4. Wait for old warrants to expire (max TTL window)

  5. Remove old key ```yaml env:
    • name: TENUO_TRUSTED_KEYS value: “NEW_KEY_HEX” ```

Rollback: Re-add old key to TENUO_TRUSTED_KEYS.


Delegation Chains (Multi-Hop)

When agents delegate to sub-agents across pods, the full delegation chain must travel with the request so each hop can be independently verified.

How It Works

Control Plane                Agent A (Pod 1)              Agent B (Pod 2)
     │                            │                            │
     │  root warrant (depth=0)    │                            │
     │ ──────────────────────────>│                            │
     │                            │  grant child warrant       │
     │                            │  (depth=1, parent_hash)    │
     │                            │                            │
     │                            │  X-Tenuo-Warrant: [root, child]  (WarrantStack)
     │                            │  X-Tenuo-PoP: <sig>        │
     │                            │ ──────────────────────────>│
     │                            │                            │  authorizer verifies
     │                            │                            │  full chain via check_chain

The X-Tenuo-Warrant header carries a WarrantStack — a base64-encoded CBOR array of warrants ordered root-first, leaf-last. The authorizer’s check_chain verifies:

  1. The root warrant’s issuer is in TENUO_TRUSTED_KEYS
  2. Each child’s parent_hash matches its parent
  3. Capabilities are monotonically attenuated (child ⊆ parent)
  4. The leaf’s PoP signature is valid

Client-Side (Python SDK)

from tenuo import Warrant, SigningKey, encode_warrant_stack

root = Warrant.mint_builder().tool("search").tool("summarize").mint(control_plane_key)

child = (
    Warrant.grant_builder(root)
    .tool("search")           # attenuate: drop summarize
    .holder(agent_b_key.public_key)
    .ttl(60)
    .grant(agent_a_key)
)

stack_b64 = encode_warrant_stack([root, child])

headers = {
    "X-Tenuo-Warrant": stack_b64,
    "X-Tenuo-PoP": pop_signature_b64,
}

Authorizer Behavior

The authorizer auto-detects whether X-Tenuo-Warrant contains a single warrant or a WarrantStack. No configuration change is needed — check_chain is always used.

If a delegated warrant (depth > 0) arrives without its parent chain, the authorizer logs a warning:

WARN Received orphaned delegated warrant (depth > 0) without parent chain.
     Zero-Trust best practice is to send full WarrantStack.

Helm Chart: Trusted Roots for Chains

Only the root issuer needs to be in trustedRoots. Intermediate agents don’t need to be pre-registered — their authority is proven by the chain itself:

config:
  trustedRoots:
    - "f32e74b5..."   # Control plane key only

Security Checklist

Practice Why
K8s Secrets, not ConfigMaps Secrets can be encrypted at rest
SigningKey per workload Limits blast radius
Short TTLs (60-300s) Stolen warrants expire quickly
Network policies Restrict control plane access
Verify SA tokens in control plane Prevent warrant spoofing

SigningKey Strategy

Strategy Security Complexity
Per-pod Best High
Per-deployment Good Medium
Shared Weak Low

Debugging Denials

Structured Logs

{
  "level": "warn",
  "event": "authorization_denied",
  "reason": "constraint_violation",
  "tool": "read_file",
  "constraint": "path",
  "expected": "Pattern(/data/*)",
  "actual": "/etc/passwd"
}

Tail Denials

# All denials
kubectl logs -l app=tenuo-authorizer -f | \
  jq 'select(.event == "authorization_denied")'

# By tool
kubectl logs -l app=tenuo-authorizer -f | \
  jq 'select(.tool == "read_file")'

# Count by reason (last hour)
kubectl logs -l app=tenuo-authorizer --since=1h | \
  jq -r 'select(.event == "authorization_denied") | .reason' | \
  sort | uniq -c | sort -rn

Debug Headers (Non-Production)

Enable via the gateway config file (not an env var):

# gateway.yaml (or Helm: config.debugMode: true)
settings:
  debug_mode: true   # Non-production only!

Denied responses include X-Tenuo-Deny-Reason:

HTTP/1.1 403 Forbidden
X-Tenuo-Deny-Reason: constraint_violation: path=/etc/passwd not in Pattern(/data/*)

Common Denial Reasons

Reason Cause Fix
tool_not_in_warrant Tool not authorized Issue warrant with correct tools
constraint_violation Argument out of bounds Check constraints or widen warrant
warrant_expired TTL passed Issue new warrant or increase TTL
missing_pop No PoP signature Ensure keypair is set in context
invalid_pop Wrong keypair Check keypair matches warrant holder
chain_invalid Broken delegation chain Verify issuer chain

CLI Debugging

# Decode and inspect warrant contents
tenuo decode $WARRANT

# Output shows: ID, issuer, holder, tools, TTL, constraints

Monitoring

Structured Logs (Primary)

The authorizer emits structured JSON logs for every authorization decision. Use these as your primary observability signal:

# Count allow/deny decisions per tool (last hour)
kubectl logs -l app=tenuo-authorizer --since=1h | \
  jq -r 'select(.event | test("authorization_")) | "\(.event) \(.tool)"' | \
  sort | uniq -c | sort -rn

/status Endpoint

The authorizer exposes a /status endpoint (no auth required) for debugging:

# From the agent container in the same pod (distroless authorizer has no shell):
kubectl exec deploy/your-agent -c agent -- curl -s localhost:9090/status | jq

# Or port-forward and query from your machine:
kubectl port-forward deploy/your-agent 9090:9090 &
curl -s localhost:9090/status | jq
{
  "version": "0.1.0-beta.16",
  "uptime_secs": 42,
  "cp": {
    "enabled": true,
    "status": "registered",
    "authorizer_id": "tnu_auth_..."
  }
}

Use cp.status to verify control plane registration in readiness checks.

Helm ServiceMonitor (Optional)

The Helm chart includes a ServiceMonitor template for the Prometheus Operator. Enable it in values.yaml:

metrics:
  serviceMonitor:
    enabled: true
    labels:
      release: prometheus   # match your Prometheus selector
    interval: 30s

Note: The authorizer currently reports metrics to the Tenuo control plane via heartbeat rather than exposing a Prometheus /metrics endpoint. The ServiceMonitor monitors the /health endpoint for up/down status. For detailed authorization metrics (allow/deny counts, latency percentiles), use structured logs or the control plane dashboard.


See Also