Secure Your Agent
This walkthrough takes you from an existing AI agent to a fully secured PRECINCT deployment. Choose the track that matches your environment: Docker Compose for local development, or Kubernetes for enterprise and production workloads.
Overview
PRECINCT can secure any agent that speaks MCP (Model Context Protocol) without modifying the agent's source code. The gateway sits between your agent and its tools, enforcing 13 layers of security controls transparently. Choose the track that fits your deployment target.
Track A: Home User / Developer
Docker Compose on a laptop or workstation. Ideal for evaluation, demos, and local development.
- Time: ~15 minutes
- Requires: Docker Desktop
- Result: fully functional security stack on localhost
Track B: Enterprise / Platform Team
Kubernetes for staging and production environments. Full network policies, admission control, and SPIFFE mTLS.
- Time: ~30 minutes
- Requires: kubectl, kustomize, running K8s cluster
- Optional: Helm for values-driven configuration
Prerequisites
The following table shows what each track requires. All items in Track A are mandatory for Docker Compose. Track B items are required only for Kubernetes deployments.
| Requirement | Track A (Compose) | Track B (K8s) |
|---|---|---|
| Docker + Docker Compose | Required (v24+ / v2.20+) |
Required (for image builds) |
| Go | Required (1.22+) |
Required (1.22+) |
| Make | Required | Required |
| kubectl | Not needed | Required (1.28+) |
| kustomize | Not needed | Required (5.0+) |
| Helm | Not needed | Optional (3.12+) |
Track A: Docker Compose
This track walks you through adding your agent to the existing PRECINCT Docker Compose stack. Each step builds on the previous one. By the end, your agent will have a cryptographic identity, policy-governed access to tools, and full audit logging.
Step 1: Add your agent service to docker-compose.override.yml
Create a docker-compose.override.yml in the
POC/ directory. Docker Compose automatically merges this
file with the base docker-compose.yml. Your agent
container must share the agentic-net network to reach the
gateway and the SPIRE agent socket volume to obtain its SVID.
# POC/docker-compose.override.yml
services:
my-agent:
build:
context: ./path/to/your/agent
dockerfile: Dockerfile
container_name: my-agent
hostname: my-agent
depends_on:
mcp-security-gateway:
condition: service_healthy
volumes:
- spire-agent-socket:/tmp/spire-agent/public:ro
environment:
- MCP_GATEWAY_URL=http://mcp-security-gateway:9090
- SPIFFE_ENDPOINT_SOCKET=unix:///tmp/spire-agent/public/api.sock
networks:
- agentic-net
labels:
- "spiffe-id=my-agent"
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
tmpfs:
- /tmp
Step 2: Register a SPIRE identity for your agent
Each workload in PRECINCT receives a SPIFFE identity via SPIRE. Add a registration entry so SPIRE will issue an SVID to your agent based on its Docker label.
# Add this to scripts/register-spire-entries.sh (before the final echo)
/opt/spire/bin/spire-server entry create \
-socketPath /tmp/spire-server/private/api.sock \
-parentID spiffe://poc.local/agent/local \
-spiffeID spiffe://poc.local/agents/mcp-client/my-agent/dev \
-selector docker:label:spiffe-id:my-agent \
-ttl 3600
Step 3: Write an OPA policy for your agent
Create a Rego policy file that authorizes your agent's SPIFFE ID
to invoke specific tools. Place it in config/opa/ so
the gateway loads it automatically.
# config/opa/my_agent_policy.rego
package mcp.authz
import rego.v1
# Allow my-agent to call tavily_search and echo tools
allow if {
input.caller == "spiffe://poc.local/agents/mcp-client/my-agent/dev"
input.tool in {"tavily_search", "echo"}
}
Step 4: Register tools in the capability registry
If your agent uses tools that are not already registered, add them to
config/capability-registry-v2.yaml. Each tool needs a
name, description, and SHA-256 hash of its schema.
# Append to config/capability-registry-v2.yaml under the tools section
- name: my_custom_tool
description: "Tool provided by my-agent"
inputSchema:
type: object
properties:
query:
type: string
required: ["query"]
upstream: http://my-tool-server:8080/mcp
sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
Step 5: Route traffic through the gateway
Configure your agent to send all MCP requests to the PRECINCT gateway
instead of directly to tool servers. The gateway URL is
http://mcp-security-gateway:9090 from within the Docker
network.
# Example: Python agent using the PRECINCT SDK
from mcp_gateway_sdk import GatewayClient
client = GatewayClient(
gateway_url="http://mcp-security-gateway:9090",
spiffe_id="spiffe://poc.local/agents/mcp-client/my-agent/dev"
)
# All tool calls now flow through the 13-layer security chain
result = await client.call_tool("tavily_search", {"query": "AI safety"})
Step 6: Verify end-to-end
Start the stack and confirm that your agent's requests are being processed through PRECINCT's security layers.
# Start the full stack (includes your override)
cd POC
make up
# Verify your agent can call tools through the gateway
curl -s http://localhost:9090/ \
-H "Content-Type: application/json" \
-H "X-SPIFFE-ID: spiffe://poc.local/agents/mcp-client/my-agent/dev" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}' | jq .
# Verify unauthorized SPIFFE IDs are denied
curl -s http://localhost:9090/ \
-H "Content-Type: application/json" \
-H "X-SPIFFE-ID: spiffe://poc.local/agents/unknown/attacker" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"tavily_search","arguments":{"query":"test"}},"id":2}' | jq .code
# Expected: "authz_policy_denied"
# Check audit logs for your agent's requests
docker exec mcp-security-gateway cat /tmp/audit.jsonl | jq 'select(.caller_id | contains("my-agent"))'
Your agent is now secured by PRECINCT. Every request flows through identity verification, policy evaluation, DLP scanning, rate limiting, and tamper-evident audit logging.
Track B: Kubernetes
This track deploys your agent into a Kubernetes cluster alongside the
PRECINCT stack. It assumes you have a running cluster with PRECINCT
already deployed (via make k8s-up or kustomize).
Step 1: Deploy your agent as a Kubernetes Deployment
Create a Deployment in the tools namespace. Mount the
SPIRE agent socket via hostPath so your agent can obtain its SVID.
# infra/eks/my-agent/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-agent
namespace: tools
labels:
app.kubernetes.io/name: my-agent
app.kubernetes.io/component: agent
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: my-agent
template:
metadata:
labels:
app.kubernetes.io/name: my-agent
spec:
serviceAccountName: my-agent
containers:
- name: my-agent
image: your-registry/my-agent:latest
env:
- name: MCP_GATEWAY_URL
value: "http://mcp-security-gateway.gateway.svc.cluster.local:9090"
- name: SPIFFE_ENDPOINT_SOCKET
value: "unix:///run/spire/sockets/agent.sock"
volumeMounts:
- name: spire-agent-socket
mountPath: /run/spire/sockets
readOnly: true
securityContext:
runAsNonRoot: true
runAsUser: 65532
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
volumes:
- name: spire-agent-socket
hostPath:
path: /run/spire/sockets
type: DirectoryOrCreate
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-agent
namespace: tools
Step 2: Create a SPIRE entry for your agent
Register your agent's SPIFFE ID with SPIRE. On Kubernetes, SPIRE uses
the k8s_psat (projected service account token) attestor
to identify workloads. For local Docker Desktop clusters using join
token attestation, use the agent's SPIFFE ID as the parent.
# For k8s_psat attestation (EKS, GKE, AKS):
kubectl exec -n spire-system spire-server-0 -- \
/opt/spire/bin/spire-server entry create \
-spiffeID spiffe://agentic-ref-arch.poc/ns/tools/sa/my-agent \
-parentID spiffe://agentic-ref-arch.poc/spire/agent/k8s_psat/default \
-selector k8s:ns:tools \
-selector k8s:sa:my-agent \
-ttl 3600
# For local Docker Desktop (join_token attestation):
kubectl exec -n spire-system spire-server-0 -- \
/opt/spire/bin/spire-server entry create \
-spiffeID spiffe://agentic-ref-arch.poc/ns/tools/sa/my-agent \
-parentID spiffe://agentic-ref-arch.poc/agent/local \
-selector k8s:ns:tools \
-selector k8s:sa:my-agent \
-ttl 3600
Step 3: Write an OPA policy
Add your agent's authorization policy to the gateway ConfigMap. The
SPIFFE ID in Kubernetes uses the trust domain
agentic-ref-arch.poc and the namespace/service account
path format.
# Add to gateway-config ConfigMap (or create a new .rego file)
package mcp.authz
import rego.v1
# Allow my-agent (K8s SPIFFE ID) to call specific tools
allow if {
input.caller == "spiffe://agentic-ref-arch.poc/ns/tools/sa/my-agent"
input.tool in {"tavily_search", "echo"}
}
Step 4: Register tools via ConfigMap
Update the gateway-config ConfigMap to include any new
tools your agent requires. The ConfigMap is mounted into the gateway
pod at /config/.
# Edit the gateway-config ConfigMap directly
kubectl edit configmap gateway-config -n gateway
# Or update the kustomization overlay file and re-apply
kustomize build infra/eks/overlays/local/ | kubectl apply -f -
Step 5: Apply a NetworkPolicy
Restrict your agent's network egress so it can only reach the gateway. This prevents agents from bypassing PRECINCT by calling tool servers directly.
# infra/eks/my-agent/networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: my-agent-egress
namespace: tools
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: my-agent
policyTypes:
- Egress
egress:
# Allow traffic to the gateway only
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: gateway
podSelector:
matchLabels:
app.kubernetes.io/name: mcp-security-gateway
ports:
- protocol: TCP
port: 9090
# Allow DNS resolution
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
# Allow SPIRE agent socket (same node)
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: spire-system
Step 6: Verify via port-forward
Forward the gateway port and send test requests to confirm your agent can reach the gateway and that policies are enforced.
# Apply your manifests
kubectl apply -f infra/eks/my-agent/
# Port-forward the gateway
kubectl port-forward -n gateway svc/mcp-security-gateway 9090:9090 &
# Test your agent's SPIFFE ID
curl -s http://localhost:9090/ \
-H "Content-Type: application/json" \
-H "X-SPIFFE-ID: spiffe://agentic-ref-arch.poc/ns/tools/sa/my-agent" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}' | jq .
# Run the E2E demo
make demo-k8s
EKS: Use IRSA (IAM Roles for Service Accounts)
for cloud resource access. SPIRE uses the k8s_psat
node attestor with projected service account tokens. ALB Ingress
replaces NodePort.
GKE: Workload Identity Federation maps Kubernetes
service accounts to Google Cloud service accounts. Use GKE Autopilot
for managed node pools.
AKS: Azure AD Workload Identity provides pod-level
identity. Use Azure CNI for NetworkPolicy enforcement.
Your agent is secured in Kubernetes with SPIFFE mTLS, OPA policy evaluation, NetworkPolicy isolation, and production-grade audit logging.
Under the Hood
When your agent sends a request to the PRECINCT gateway, it passes through a 13-layer middleware chain. Each layer enforces a specific security control. The request must pass every layer to reach the upstream tool server. Here is the complete request journey:
| Layer | Name | Purpose | Deny Code |
|---|---|---|---|
| 1 | Request Size Limiter | Rejects oversized payloads before parsing | request_too_large |
| 2 | JSON-RPC Validator | Validates JSON-RPC 2.0 envelope structure | invalid_jsonrpc |
| 3 | SPIFFE Authentication | Validates caller identity (X-SPIFFE-ID header or mTLS SVID) | spiffe_auth_required |
| 4 | Audit Logger | Creates hash-chained, tamper-evident audit record | -- |
| 5 | Tool Registry | Validates tool exists and schema hash matches | tool_not_in_registry |
| 6 | OPA Policy Evaluation | Authorizes caller + tool + arguments against Rego policy | authz_policy_denied |
| 7 | DLP Scanner | Detects credentials, PII, and secrets in request body | dlp_credentials_detected |
| 8 | Session Tracking | Maintains per-identity session state and cumulative risk score | -- |
| 9 | Step-Up Authentication | Triggers re-authentication if risk score exceeds threshold | stepup_required |
| 10 | Deep Scan (Guard Model) | LLM-based prompt injection and jailbreak detection | deep_scan_denied |
| 11 | Rate Limiter | Per-identity token-bucket rate limiting via KeyDB | rate_limit_exceeded |
| 12 | Circuit Breaker | Prevents cascade failures when upstream is degraded | circuit_breaker_open |
| 13 | Token Substitution | Replaces token references with secrets from SPIKE Nexus | -- |
After passing all 13 layers, the request is forwarded to the upstream tool server via MCP Streamable HTTP. The response follows the reverse path: the audit logger records the outcome, and the DLP scanner checks the response body for data exfiltration attempts.
Troubleshooting
Network Connectivity
Symptom: Agent cannot reach the gateway; connection refused or timeout.
-
Compose: Verify your agent is on the
agentic-netnetwork. Rundocker network inspect agentic-security-networkto confirm. -
K8s: Check that the gateway Service is accessible
from your agent's namespace. Use
kubectl exec -n tools my-agent-pod -- wget -qO- http://mcp-security-gateway.gateway.svc.cluster.local:9090/healthto test. - Ensure no NetworkPolicy is blocking egress from your agent to the gateway.
SPIRE Timing / SVID Fetch Delays
Symptom: Agent starts but gets spiffe_auth_required errors for the first 5-10 seconds.
- SVID fetch on first request takes 5-10 seconds while the SPIRE agent attests the workload and delivers the certificate. Add a startup delay or retry loop.
-
In Compose, ensure
spire-entry-registrarhas completed successfully before your agent starts. Usedepends_on: spire-entry-registrar: condition: service_completed_successfully. -
In K8s, the SPIRE DaemonSet must be healthy before your pods start.
Add an init container that waits for the SPIRE socket:
[ -S /run/spire/sockets/agent.sock ].
OPA Policy Denials
Symptom: Requests return authz_policy_denied.
- Verify that the SPIFFE ID in your Rego policy exactly matches the one registered in SPIRE. Case and path segments matter.
-
Check the tool name in the policy matches the
tools/callrequest'sparams.namefield. -
Test your policy locally with
opa eval:opa eval -d config/opa/ -i input.json "data.mcp.authz.allow"
DLP False Positives
Symptom: Legitimate requests blocked with dlp_credentials_detected.
- The DLP scanner uses regex patterns to detect AWS keys, API tokens, and other credentials. If your payload contains strings that match these patterns (e.g., base64-encoded data), it may trigger a false positive.
-
Set
DLP_INJECTION_POLICY=flag(instead ofblock) to log violations without blocking. Review the audit log to tune patterns. -
For testing, you can temporarily disable DLP by setting the
enforcement profile to
dev(not recommended for production).
Next Steps
Your agent is secured. Here are the recommended next steps to deepen your integration and prepare for production.
Integration Guide
Detailed reference for identity registration, tool configuration, policy authoring, and traffic routing patterns.
SDKs
First-class Python and Go SDKs that handle SPIFFE identity injection, structured error handling, and gateway communication.
Case Study
See how a real agentic AI application was secured by PRECINCT without any code changes.
For Auditors
Compliance framework mappings for SOC 2, NIST 800-53, and ISO 27001. Evidence collection and control documentation.