Securing agents with policies
This tutorial walks through securing a customer service agent step by step. You’ll restrict tool access, propagate user identity, enforce business rules with CEL, and validate everything in audit mode before going live.
Scenario
Section titled “Scenario”You have a customer service agent (support-agent) with access to a customer-tools ToolRegistry containing:
lookup_order— look up order detailscheck_status— check order statusprocess_refund— issue refundsdelete_account— delete a customer account
The agent should be able to look up orders and process refunds, but not delete accounts. Refunds should be capped at $500 and require a reason. The user’s team identity must flow through to downstream services.
Prerequisites
Section titled “Prerequisites”- A running Omnia cluster with Istio enabled — and, under ambient mode, a waypoint proxy enrolled for the
support-agentService, since AgentPolicy’stoolAccessrules match on theX-Omnia-Tool-NameHTTP header (an L7 attribute) and ztunnel alone only enforces L4. Without a waypoint, the generated AuthorizationPolicy is created but never enforced. - JWT authentication configured (see Configure Agent Authentication)
- The
support-agentAgentRuntime andcustomer-toolsToolRegistry deployed
Step 1: restrict tool access with AgentPolicy
Section titled “Step 1: restrict tool access with AgentPolicy”Start by limiting which tools the agent can call. Create an AgentPolicy with a tool allowlist:
apiVersion: omnia.altairalabs.ai/v1alpha1kind: AgentPolicymetadata: name: support-agent-policy namespace: productionspec: selector: agents: - support-agent
toolAccess: mode: allowlist rules: - registry: customer-tools tools: - lookup_order - check_status - process_refund # delete_account is deliberately excludedApply it:
kubectl apply -f support-agent-policy.yamlVerify the policy is active:
kubectl get agentpolicies -n productionNAME MODE PHASE MATCHED AGEsupport-agent-policy enforce Active 1 10sThe agent can now only call lookup_order, check_status, and process_refund. Any attempt to call delete_account is blocked at the Istio network level — provided the agent’s Service is enrolled behind a waypoint (see prerequisites); plain ambient mode with no waypoint leaves the generated AuthorizationPolicy unenforced.
Step 2: forward user identity claims
Section titled “Step 2: forward user identity claims”AgentPolicy has no claim-mapping configuration — it only governs tool allow/deny. Claim forwarding to downstream tools is configured on the support-agent AgentRuntime’s external-auth block (spec.externalAuth.oidc.claimMapping, already set up as part of the JWT authentication prerequisite — see Configure Agent Authentication).
Once that’s in place, the facade extracts the configured claims from the verified JWT and forwards them as X-Omnia-Claim-* headers on every tool call — for example X-Omnia-Claim-Team and X-Omnia-Claim-Customer-Id — with no further AgentPolicy changes needed.
Step 3: add business rules with ToolPolicy
Section titled “Step 3: add business rules with ToolPolicy”Create a ToolPolicy with CEL rules to enforce refund limits and require a reason:
apiVersion: omnia.altairalabs.ai/v1alpha1kind: ToolPolicymetadata: name: refund-guardrails namespace: productionspec: selector: registry: customer-tools tools: - process_refund
requiredClaims: - claim: Team message: "Team claim is required for refund operations" - claim: Customer-Id message: "Customer ID is required for refund operations"
rules: - name: max-refund-amount description: "Cap refunds at $500" deny: cel: 'has(body.amount) && double(body.amount) > 500.0' message: "Refund amount exceeds the $500 limit"
- name: require-reason description: "All refunds must include a reason" deny: cel: '!has(body.reason) || body.reason == ""' message: "A reason is required for refund requests"
headerInjection: - header: X-Processed-By cel: 'headers["X-Omnia-Claim-Team"]'
mode: audit # Start in audit mode onFailure: denyApply it:
kubectl apply -f refund-guardrails.yamlVerify:
kubectl get toolpolicies -n productionNAME REGISTRY MODE PHASE RULES AGErefund-guardrails customer-tools audit Active 2 10sStep 4: validate in audit mode
Section titled “Step 4: validate in audit mode”With mode: audit, the policy logs violations but does not block requests. This lets you verify the rules are matching correctly before enforcement.
Test the agent by making a refund call that violates the rules (e.g., amount > $500). Then check the policy-broker logs (the broker runs as a sidecar in the support-agent agent pod, not on customer-tools). Agent pods carry a fixed app.kubernetes.io/name=omnia-agent label across every AgentRuntime, so select on app.kubernetes.io/instance (the AgentRuntime name) to target this one agent:
kubectl logs -n production -l app.kubernetes.io/instance=support-agent -c policy-broker | grep policy_decisionYou should see a pair of audit log lines like:
{"msg":"policy_decision","allowed":true,"deniedBy":"max-refund-amount","message":"Refund amount exceeds the $500 limit","mode":"audit","policy":"refund-guardrails"}{"msg":"broker_tool_decision","toolName":"process_refund","toolRegistry":"customer-tools","allowed":true,"deniedBy":"max-refund-amount","mode":"audit"}In audit mode a matched deny rule still sets deniedBy and message, but allowed stays true — the call is let through with the violation logged. That combination ("mode":"audit" + non-empty deniedBy) is exactly what would flip to a hard denial once the policy switches to mode: enforce.
Step 5: switch to enforce mode
Section titled “Step 5: switch to enforce mode”Once audit logs confirm the policy is working correctly, switch to enforce mode:
spec: mode: enforcekubectl apply -f refund-guardrails.yamlVerify the mode changed:
kubectl get toolpolicies -n productionNAME REGISTRY MODE PHASE RULES AGErefund-guardrails customer-tools enforce Active 2 1hNow any refund over $500 or without a reason is denied — the broker returns allow: false and the runtime aborts the tool call instead of invoking it:
{ "allow": false, "deniedBy": "max-refund-amount", "message": "Refund amount exceeds the $500 limit", "mode": "enforce", "wouldDeny": false, "injectedHeaders": null}What you’ve built
Section titled “What you’ve built”Here’s the complete security architecture for your agent:
graph TB CLIENT[Client + JWT] --> ISTIO[Istio Sidecar]
subgraph "AgentPolicy Enforcement" ISTIO -->|Tool allowlist check| FACADE[Facade] end
FACADE -->|"external-auth: extract team, customer_id"| RUNTIME[Runtime PEP]
subgraph "ToolPolicy Enforcement" RUNTIME -->|"POST /v1/decision: headers + body + identity"| BROKER[Policy Broker PDP] BROKER -->|Check required claims| BROKER BROKER -->|Evaluate CEL rules| BROKER BROKER -->|"200 {allow, injectedHeaders: X-Processed-By}"| RUNTIME end
RUNTIME -->|"Denied: dispatch aborted, tool-call error returned"| RUNTIME RUNTIME -->|Allowed: call tool with injected headers| TOOL[Tool Service]AgentPolicy provides:
- Tool allowlist —
delete_accountblocked at the network level
AgentRuntime external-auth provides:
- Claim mapping —
teamandcustomer_idpropagated asX-Omnia-Claim-*headers
ToolPolicy provides:
- Required claims — team and customer ID must be present
- CEL rules — refund amount cap and reason requirement
- Header injection — team identity forwarded to the tool service (applied by the runtime, only when allowed)
- Audit logging — full decision trail
Next steps
Section titled “Next steps”- Add more CEL rules for other tools in the registry
- Create policies for other agents in the namespace
- Review the AgentPolicy Reference and ToolPolicy Reference for all available fields
Related resources
Section titled “Related resources”- Policy Engine Architecture — how the policy engine works
- AgentPolicy CRD Reference — field-by-field specification
- ToolPolicy CRD Reference — field-by-field specification
- Configure Agent Policies — operational guide
- Configure Tool Policies — operational guide