shield360 proxy
shield360 proxy serve is a local AI gateway. Apps, SDKs and LiteLLM send their provider calls to it instead of straight to the provider. Before a request leaves, it enforces your organisation’s Shield360 AI policy, which is the same policy the Shield360 browser extension enforces:
- AI tools. A provider your organisation blocks (
aiTools) is refused. One set to warn is forwarded and recorded. - Approved models. When the organisation warns or blocks outside the approved list, a request for a model or provider not on it (
approvedModels) is refused withmodel_not_approved, or forwarded and recorded. - Prompt protection. Sensitive data in a prompt (
aiGuard, with per-kind overrides inaiGuardKinds) is warned about, masked or blocked. - Prompt injection. The newest turn of each request, meaning the new user message and any tool results the client sends back, is checked with the same rules as the coding-agent hooks. It follows the organisation’s prompt-injection setting for coding agents (
injectionMode): warn forwards the request and records a detection, block refuses it withprompt_injection. Earlier turns and system prompts are not checked. The hosted proxy does not check for prompt injection yet. See Prompt-injection protection. - Evidence. Each decision is reported to Shield360 as a guard event with source AI proxy. The event records the kinds of data and their keyed fingerprints, shown as tokens such as
[api_key:fp_3f9c2a1b]. It never contains any character of the values. See Data retention.
This is API gateway mode: clients opt in by changing their base URL, and there is no TLS interception.
There are two ways to run it:
- Hosted by Shield360 (Shield360 SaaS). Shield360 runs the proxy at
https://proxy.shield360.ai. There’s nothing to install: create a proxy key and change your apps’ base URL. See Hosted proxy. - Run it yourself: on a machine, as a service, or as a container in your own network. The rest of this page covers that.
Hosted proxy (Shield360 SaaS)
Section titled “Hosted proxy (Shield360 SaaS)”On Shield360 SaaS, the AI proxy panel under Settings → Collection & deployment has a Hosted by Shield360 tab, shown first. Self-hosted deployments don’t show it.
Give the key a name and, optionally, an expiry. The key starts with s360p_ and is shown once. Shield360 stores only its SHA-256 hash. A key belongs to the environment that is selected in the header, and its requests are enforced with that environment’s policy and recorded in its data.
The key is part of the base URL. Apps keep sending their own provider API key, as usual:
OPENAI_BASE_URL=https://proxy.shield360.ai/t/<proxy-key>/openai/v1ANTHROPIC_BASE_URL=https://proxy.shield360.ai/t/<proxy-key>/anthropicGOOGLE_GEMINI_BASE_URL=https://proxy.shield360.ai/t/<proxy-key>/geminiFor LiteLLM, set api_base: https://proxy.shield360.ai/t/<proxy-key>/openai/v1. The panel fills your key into these while it’s shown.
Decisions appear under Detect & respond → Detections → Guard events with source AI proxy and host hosted-proxy, and in Govern → Policies & rules → Violations. The panel shows when each key was last used.
To stop a key, revoke it in the panel. The proxy stops accepting it within a minute. A key past its expiry stops the same way.
What passes through Shield360
Section titled “What passes through Shield360”- Data flow. Requests pass through Shield360’s proxy in AWS us-east-2 (Ohio). TLS ends at Shield360’s load balancer. Prompts and your provider API keys are inspected in memory and passed on to the provider. They’re never stored or logged.
- What’s logged. The method, the provider, the path after the provider name, the status, the action and the kinds found. Never the proxy key or anything under
/t/, request or response bodies, query strings, orAuthorization,x-api-key,api-key,x-goog-api-keyor cookie headers. - What’s recorded. The same guard event as a self-run proxy: kinds and keyed fingerprints. Never any character of the values.
- What the provider sees. Your API key and your request, with sensitive data masked or the request refused, as your policy says.
X-Shield360-*headers and the proxy key are removed first. The request comes from Shield360’s IP addresses. If your provider account only accepts requests from your own IP addresses, run the proxy yourself. - Fixed providers. The hosted proxy forwards only to
api.openai.com,api.anthropic.comandgenerativelanguage.googleapis.com. For Azure OpenAI, LiteLLM or another upstream, run the proxy yourself, or put the hosted proxy in front of LiteLLM’s provider calls.
Limits
Section titled “Limits”Each organisation environment has its own limits, so one customer can’t slow down another:
| Limit | Default | When it’s reached |
|---|---|---|
| Requests in flight | 32 | 429, provider error shape, Retry-After: 1 |
| Request rate | 20 per second, bursts of 40 | 429, provider error shape, Retry-After: 1 |
| Concurrent inspections | A quarter of the proxy’s scan slots | The prompt counts as not inspected (see Scan limits) |
A key the proxy can’t verify gets 401 with code proxy_key_invalid, in the provider’s error shape. The response doesn’t say whether the key is unknown, revoked or expired.
If the proxy can’t reach Shield360, it keeps enforcing each key’s last known policy for up to 10 minutes. It refuses the key’s requests with policy_unavailable if that policy blocks by default and has Block when the policy is unavailable on. A key it has never verified gets 503.
Run the hosted proxy (operators)
Section titled “Run the hosted proxy (operators)”The hosted proxy is shield360 proxy serve --hosted, built from deploy/proxy/Dockerfile and deployed by deploy/k8s/shield360-{qa,prod}/hosted-proxy.yaml:
shield360 proxy serve --hosted \ --platform-url http://shield360-service.shield360-prod.svc.cluster.local \ --service-secret-file /etc/shield360-hosted-proxy/secret \ --listen 0.0.0.0:8787 --shutdown-delay 15s- Keys. The proxy sends each key to
POST /api/internal/hosted-proxy/resolve, in batches of up to 100, with the service secret inX-Shield360-Hosted-Proxy-Secret. It gets back an opaque tenant id (a keyed hash, not the database config id), the AI policy and a TTL of at most 60 seconds. Unknown keys are cached for 30 seconds. Concurrent lookups of one key share one request. - Events. Decisions are queued in memory, up to 20,000 across tenants. When the queue is full the oldest are dropped, and the count shows in the
stats:line the proxy logs every minute. They’re delivered every 2 seconds toPOST /api/internal/hosted-proxy/events, one batch per key. The app finds each batch’s tenant from its key again, and forces sourceproxyand hosthosted-proxy. - Service secret.
SHIELD360_HOSTED_PROXY_SECRET, at least 32 characters, must be the same for the app and the proxy. Without it the internal routes answer404and the panel hides the hosted tab. The app also needsSHIELD360_HOSTED_PROXY_URL, and optionallySHIELD360_HOSTED_PROXY_REGIONfor the data-flow note. - TLS. The proxy serves plain HTTP. Put it behind a TLS-terminating load balancer, such as the ALB in the manifests. It needs no enrolment and no access token, because the proxy key is the credential.
- Health.
GET /healthzis liveness.GET /readyzreturns503while the proxy is draining afterSIGTERM.
| Flag | Description |
|---|---|
--hosted | Multi-tenant mode. --upstream, --upstream-vendor, --config, --access-token-file, --install-key-file and --url are refused. |
--platform-url | The Shield360 app. Must use https, or plain http only for a loopback or in-cluster (.svc) address. SHIELD360_URL also works. |
--service-secret-file | File that holds the service secret. SHIELD360_HOSTED_PROXY_SECRET also works. |
--tenant-concurrency, --tenant-rps, --tenant-burst | Per-tenant limits. Defaults 32, 20 and 40. |
--tenant-scan-share | Concurrent inspections per tenant. Default a quarter of --scan-concurrency, at least 1. |
--global-concurrency | Requests in flight across every tenant. Default 2048. Above it, 503. |
--event-queue | Decisions held in memory for delivery. Default 20,000. |
--shutdown-delay | On SIGTERM, how long /readyz reports draining before the listener closes. |
To run the same binary as one customer’s dedicated proxy, add --pin-tenant <tenant id>: keys of any other tenant are refused.
Where the settings live
Section titled “Where the settings live”What the proxy enforces is set in Shield360, in the organisation’s enforcement policy under Govern → Policies & rules: the AI proxy column of the Sensitive data tab (and the All channels column, which it follows unless it sets its own value), the AI tools tab, the Block when the policy is unavailable toggle, and the Coding agents prompt-injection setting on the Coding agents tab. The organisation sets them once; an environment can override any one of them. The proxy is handed the policy of the environment its key or enrolment belongs to.
The platform sends the proxy’s own actions as proxyGuard and proxyGuardKinds beside the browser extension’s aiGuard and aiGuardKinds. A proxy older than that reads aiGuard and aiGuardKinds, which are the browser’s values. The hosted proxy always gets the proxy’s values.
Deployment stays on Settings → Collection & deployment, which has an AI proxy panel with:
- Connected proxies: each proxy in the current environment, with its version, whether it serves only its own machine or other machines, the policy version it enforces and when it fetched it, when it was last seen, and what it inspected and blocked since it started. A proxy that has not reported for five minutes shows as stale. You can revoke a proxy here.
- Settings that apply: a summary of the parts of the policy the proxy enforces, each linking to its tab in Govern → Policies & rules.
- Setup: the commands on this page, with your Shield360 address and install key filled in, for a machine or a container, and the base URLs for your apps.
Some settings are local to each proxy and are set with flags or a config file on the proxy host: its upstreams, listen address, access token and scan limits.
Run it
Section titled “Run it”On a machine whose collector is enrolled, the proxy uses that enrolment and there’s nothing to do. Otherwise, enrol the proxy as its own device with your organisation’s install key:
SHIELD360_INSTALL_KEY=<key> shield360 proxy enroll --url https://app.shield360.aiThe proxy enrols with kind proxy and appears in the fleet list as AI proxy. Its token is kept in <config dir>/proxy/proxy-enrollment.json with owner-only permissions. The install key isn’t stored.
shield360 proxy serveThe proxy listens on 127.0.0.1:8787. It fetches the policy before it starts serving, then fetches it again every 60 seconds.
To keep it running in the background instead, see Run it as a service.
Requests to /<upstream>/<path> go to <upstream URL>/<path>. The query string and credentials pass through unchanged.
| Upstream | Default URL | Base URL to use |
|---|---|---|
openai | https://api.openai.com | http://127.0.0.1:8787/openai/v1 |
anthropic | https://api.anthropic.com | http://127.0.0.1:8787/anthropic |
gemini | https://generativelanguage.googleapis.com | http://127.0.0.1:8787/gemini |
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8787/openai/v1") # OPENAI_API_KEY as usualfrom anthropic import Anthropic
client = Anthropic(base_url="http://127.0.0.1:8787/anthropic") # ANTHROPIC_API_KEY as usualimport OpenAI from "openai";
const client = new OpenAI({ baseURL: "http://127.0.0.1:8787/openai/v1" });from google import genai
client = genai.Client(http_options={"base_url": "http://127.0.0.1:8787/gemini"})Most SDKs also read the base URL from the environment, for example OPENAI_BASE_URL=http://127.0.0.1:8787/openai/v1 or ANTHROPIC_BASE_URL=http://127.0.0.1:8787/anthropic.
LiteLLM
Section titled “LiteLLM”To send LiteLLM’s provider calls through the proxy, set api_base on each model:
model_list: - model_name: gpt-4o litellm_params: model: openai/gpt-4o api_base: http://127.0.0.1:8787/openai/v1 api_key: os.environ/OPENAI_API_KEY - model_name: claude-sonnet litellm_params: model: anthropic/claude-sonnet-4-5 api_base: http://127.0.0.1:8787/anthropic api_key: os.environ/ANTHROPIC_API_KEYYou can also put the proxy in front of LiteLLM, so apps call the proxy and the proxy calls LiteLLM. LiteLLM speaks OpenAI’s format. Give it a vendor id so aiTools can decide on it; otherwise the default entry applies:
shield360 proxy serve --upstream litellm=http://litellm:4000 --upstream-vendor litellm=openaiName the user
Section titled “Name the user”An app that serves many people can name the person each request is for with the X-Shield360-User header. The proxy records that name on the event and removes the header before forwarding. Without the header, events name the enrolled user.
Configure tools with MDM
Section titled “Configure tools with MDM”To point every machine at the proxy without setting each one up by hand, push the configuration through Intune, Jamf or your configuration tool. Settings → Data collection → Deployment → AI proxy → Push with MDM generates each file below with the base URL filled in. Choose which proxy to point at:
| Proxy | Base URL | Token |
|---|---|---|
| Hosted by Shield360 | https://<hosted-proxy>/t/<proxy-key> (the key you just created is filled in) | In the URL |
| Local | http://127.0.0.1:8787 (each machine runs its own proxy) | None |
| Container or another machine | The URL you enter, for example http://proxy.internal:8787 | X-Shield360-Proxy-Token header |
The examples below use <base> for the base URL.
Claude Code
Section titled “Claude Code”Claude Code reads a managed settings file that overrides every user and project setting. Set ANTHROPIC_BASE_URL in its env block:
{ "env": { "ANTHROPIC_BASE_URL": "<base>/anthropic" }}Install it at:
| OS | Path |
|---|---|
| macOS | /Library/Application Support/ClaudeCode/managed-settings.json |
| Linux and WSL | /etc/claude-code/managed-settings.json |
| Windows | C:\Program Files\ClaudeCode\managed-settings.json |
Claude Code no longer reads the old Windows path C:\ProgramData\ClaudeCode\managed-settings.json. On macOS you can push the same setting as a configuration profile for the com.anthropic.claudecode preferences domain instead; the panel generates a .mobileconfig. See Claude Code’s managed settings documentation.
ANTHROPIC_BASE_URL can’t carry a header. For a container proxy with an access token, add ANTHROPIC_CUSTOM_HEADERS (Claude Code 2.1.227 or later):
{ "env": { "ANTHROPIC_BASE_URL": "http://proxy.internal:8787/anthropic", "ANTHROPIC_CUSTOM_HEADERS": "X-Shield360-Proxy-Token: <access-token>" }}Codex reads config.toml for each user ($CODEX_HOME, ~/.codex by default), so push it to each user profile. Point the built-in OpenAI provider at the proxy:
openai_base_url = "<base>/openai/v1"The built-in provider can’t send extra headers. For a container proxy with an access token, define a provider instead:
model_provider = "shield360"
[model_providers.shield360]name = "Shield360 AI proxy"base_url = "http://proxy.internal:8787/openai/v1"env_key = "OPENAI_API_KEY"wire_api = "responses"http_headers = { "X-Shield360-Proxy-Token" = "<access-token>" }You can also set OPENAI_BASE_URL machine-wide, as below. Recent Codex releases prefer openai_base_url, so check that the variable takes effect with the Codex version you run.
Machine-wide environment variables
Section titled “Machine-wide environment variables”The OpenAI and Anthropic SDKs, and LiteLLM, read their base URL from the environment. Set these for every user:
OPENAI_BASE_URL=<base>/openai/v1ANTHROPIC_BASE_URL=<base>/anthropicWindows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment]"OPENAI_BASE_URL"="<base>/openai/v1""ANTHROPIC_BASE_URL"="<base>/anthropic"[Environment]::SetEnvironmentVariable('OPENAI_BASE_URL', '<base>/openai/v1', 'Machine'); [Environment]::SetEnvironmentVariable('ANTHROPIC_BASE_URL', '<base>/anthropic', 'Machine')<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>Label</key> <string>ai.shield360.proxy-env</string> <key>ProgramArguments</key> <array> <string>/bin/sh</string> <string>-c</string> <string>launchctl setenv OPENAI_BASE_URL '<base>/openai/v1'; launchctl setenv ANTHROPIC_BASE_URL '<base>/anthropic'</string> </array> <key>RunAtLoad</key> <true/></dict></plist>OPENAI_BASE_URL="<base>/openai/v1"ANTHROPIC_BASE_URL="<base>/anthropic"Tools started after the change pick the variables up; users may need to sign out and back in. SDKs read only the base URL from the environment, not the access token header. For a container proxy with a token, add the header in the app, or use the hosted proxy, which carries its key in the URL.
What this covers
Section titled “What this covers”The configuration covers Claude Code, Codex, and apps and scripts that use the OpenAI or Anthropic SDKs or LiteLLM. It doesn’t cover Cursor, GitHub Copilot, the ChatGPT and Claude desktop apps, or web chat. Use the browser extension for those; a forward proxy is coming. For GitHub Copilot’s agent (Copilot CLI, agent mode in VS Code and the cloud agent), the coding-agent hooks and the GitHub Copilot connector cover what can be seen; see GitHub Copilot.
Run it as a service
Section titled “Run it as a service”shield360 proxy install [serve flags]shield360 proxy statusshield360 proxy uninstallinstall registers shield360 proxy serve with your user’s service manager, with the flags you give it. It needs no administrator rights.
| Platform | Service | Starts | Restarts | Logs |
|---|---|---|---|---|
| macOS | LaunchAgent ai.thinkfleet.shield360-proxy | At login | Whenever it exits | ~/Library/Logs/Shield360/proxy.log |
| Linux | systemd user service shield360-proxy | At boot (install enables lingering; if that is refused, run sudo loginctl enable-linger <user>) | On failure | journalctl --user -u shield360-proxy |
| Windows | Task Scheduler task Shield360Proxy | At logon | On failure, every minute | %LOCALAPPDATA%\Shield360\Logs\proxy.log |
Log files are rotated at 10 MB. They never contain prompts, answers, credentials or detected values.
Enrol first, or install on a machine whose collector is enrolled. On Linux without a systemd user manager, run serve under your own supervisor or use the container. Paths are made absolute, and secrets are never written into the service definition: pass the access token with --access-token-file.
status shows:
- whether the service is installed and running
- whether the proxy answers, its address, and whether it requires an access token
- which enrolment it uses
- the policy version in force and when it was last fetched
- when it last sent its heartbeat
- its upstreams
- what it inspected, blocked and could not inspect since it started
The running proxy writes this to <state dir>/proxy-status.json every minute.
Heartbeat
Section titled “Heartbeat”Every 60 seconds the proxy reports its status to Shield360, which the Connected proxies list shows. The heartbeat carries the proxy’s version, the policy version and when it was fetched, whether it listens locally or for other machines, its upstream names, and how many requests it inspected and blocked since it started. It never carries upstream URLs, credentials or anything from a request.
Run it in a container
Section titled “Run it in a container”The image runs shield360 proxy serve --listen 0.0.0.0:8787 --allow-remote as a non-root user. No registry publishes it yet, so build it from the Shield360 repository:
docker build -f deploy/proxy/Dockerfile -t shield360-proxy:local .It is configured by its environment:
| Variable | Purpose |
|---|---|
SHIELD360_URL | Your Shield360 address |
SHIELD360_INSTALL_KEY | Install key. The proxy enrols on first start, and again if its token is revoked. |
SHIELD360_PROXY_ACCESS_TOKEN | The token clients must send. Required. |
Keep /var/lib/shield360-proxy on a volume. It holds the enrolment, the policy cache and undelivered events, so the proxy keeps one device identity across restarts.
docker run -d --name shield360-proxy --restart unless-stopped \ -p 8787:8787 \ -e SHIELD360_URL=https://app.shield360.ai \ -e SHIELD360_INSTALL_KEY=<key> \ -e SHIELD360_PROXY_ACCESS_TOKEN=<token> \ -v shield360-proxy-state:/var/lib/shield360-proxy \ shield360-proxy:localServe flags go after the image name, for example --upstream litellm=http://litellm:4000 --upstream-vendor litellm=openai. deploy/proxy/docker-compose.yml is a Compose sample. deploy/proxy/kubernetes.yaml is a minimal Deployment and Service that reads a Secret and is ready when /healthz answers.
Access token
Section titled “Access token”A proxy that other machines can reach sends provider calls for anyone who can connect to it. With --allow-remote, it requires an access token, and it refuses to start without one. The token is read from the file named by --access-token-file, or from SHIELD360_PROXY_ACCESS_TOKEN. It is never read from a flag value and never logged. It must be at least 16 printable characters. To make one:
openssl rand -base64 32 | tr -d '=+/' > proxy-token && chmod 600 proxy-tokenThe Generate token button on the AI proxy panel makes one in your browser. It is shown once and never sent to Shield360.
Clients send the token in either header:
X-Shield360-Proxy-Token: <token>Proxy-Authorization: Bearer <token>It is not the Authorization header, which carries the provider’s API key and passes through unchanged. The proxy compares tokens in constant time and removes both headers before forwarding. A request without a valid token gets 401 in the provider’s error format, or 407 when it used Proxy-Authorization. /healthz needs no token, so it works as a readiness probe.
import osfrom openai import OpenAI
client = OpenAI( base_url="http://shield360-proxy:8787/openai/v1", default_headers={"X-Shield360-Proxy-Token": os.environ["SHIELD360_PROXY_ACCESS_TOKEN"]},)For LiteLLM, add the header to each model:
litellm_params: api_base: http://shield360-proxy:8787/openai/v1 extra_headers: X-Shield360-Proxy-Token: os.environ/SHIELD360_PROXY_ACCESS_TOKENScan limits
Section titled “Scan limits”Checking a prompt runs the detector’s regular expressions over its text. On adversarial text this costs roughly a second of CPU per megabyte, so each request has an inspection budget:
| Limit | Default | Flag |
|---|---|---|
| Prompt text inspected per request | 2 MB | --max-scan-mb |
| Time one request’s inspection may take, including waiting for a slot | 500 ms | --scan-timeout |
| Inspections running at once | Number of CPUs, at least 2 | --scan-concurrency |
A scan that runs past its time keeps its slot until it finishes, so scanning never uses more than the concurrency limit’s worth of CPU.
A request over the budget is not inspected:
- When Prompt protection is set to Block, the request is refused with
413in the provider’s error format and codetoo_large_to_inspect. Ablockedevent is recorded. - Otherwise the request is forwarded unchanged, and a
warnedevent is recorded with the kinduninspected. It shows as Not inspected (too large) in Detect & respond → Detections → Guard events.
What each action does
Section titled “What each action does”| Action | Request | Response | Event |
|---|---|---|---|
off, or nothing found | Forwarded unchanged | Unchanged | None |
warn | Forwarded unchanged. There is no person to ask, so the proxy records the request and lets it through. | Unchanged | warned |
mask | Each value is replaced with a placeholder such as [EMAIL_1] or [PAYMENT_CARD_1] before it is forwarded | Placeholders are replaced with the original values | redacted |
block | Refused with 403 in the provider’s own error format. The message names the kinds found, never the values. | — | blocked |
The action for a request is the strictest of the actions for the kinds it contains. Each kind uses its aiGuardKinds entry, or aiGuard when it has none. The proxy detects the same kinds as the browser extension: API keys, private keys, passwords, connection strings, social security numbers, payment cards, tokens, cloud credentials, email addresses, phone numbers and IBANs. IP addresses are checked only when you pass --detect-ip or the policy names ip_address, because version numbers look like addresses.
A refusal uses the provider’s error shape, so SDKs raise their usual error:
{"error": {"message": "Shield360 blocked this request: ...", "type": "shield360_blocked", "code": "sensitive_data"}}Anthropic clients get {"type":"error","error":{"type":"permission_error",...}} and Gemini clients get {"error":{"code":403,"status":"PERMISSION_DENIED",...}}.
Mask and restore
Section titled “Mask and restore”With mask, the provider sees only placeholders. Numbering is per kind and the same value always gets the same placeholder, so the model can still refer to “[EMAIL_1]” consistently. The placeholders match those the browser extension writes.
The proxy keeps the map from placeholders to values in memory for that one request, and discards it once the response has been sent. Before it goes to your client, the response is rewritten:
- Non-streaming JSON. Every string in the answer is restored, including tool-call arguments.
- Streaming (server-sent events). This covers OpenAI Chat Completions and Responses, Anthropic Messages and Gemini
streamGenerateContent, including Gemini’s JSON-array form withoutalt=sse. A model can emit one placeholder across several deltas ("[EMA","IL_1]"), so each stream holds back a trailing fragment that could still become a placeholder, up to the length of the longest one. It releases the fragment as soon as the next delta completes the placeholder or shows it was ordinary text. Anything still held back at the end is sent before the stream finishes.
Values in tool-call arguments are masked inside their JSON strings, so the arguments stay valid JSON.
Policy and failure
Section titled “Policy and failure”- The proxy caches the last policy it fetched on disk, with owner-only permissions, and uses it when Shield360 can’t be reached.
- A
401from Shield360 means the token was revoked. When an install key is available, fromSHIELD360_INSTALL_KEYor--install-key-file, the proxy enrols again and continues. - The proxy fails closed only when it has no policy at all and the last policy it saved had
guardFailClosedon withaiGuardset toblock. It then refuses every request with apolicy_unavailableerror. In every other case it forwards requests unchecked and logs a warning. The warning never includes content. - Events it can’t deliver are queued in
<state dir>/guard-events-pending.jsonl, with owner-only permissions, and sent with the next batch.
GET /healthz reports whether a policy is in force and its version.
Budget enforcement
Section titled “Budget enforcement”The single-tenant proxy also applies budget enforcement. After the approved-models check it asks Shield360’s budget decision for the person (X-Shield360-User, else the enrolled user), the vendor and the model, with the prompt’s input priced at the model’s list price as the request’s estimate. Decisions are cached for 60 seconds per person, vendor and model under <state dir>/budget.
| Decision | Proxy |
|---|---|
| Allow | Forwards. |
| Warn | Forwards, with X-Shield360-Budget-Warning on the response. |
| Throttle | Forwards within the person’s rate; past it, 429 with code budget_throttled. |
| Block | 403 with code budget_exceeded, naming who to ask and the override link. |
If Shield360 can’t be reached it forwards (fails open), unless the last decision said to fail closed or SHIELD360_BUDGET_FAIL_CLOSED=1 is set; then it refuses with budget_unavailable. SHIELD360_BUDGET_ENFORCEMENT=off turns the check off on this machine. The hosted proxy does not apply budgets yet.
Agent authorization
Section titled “Agent authorization”When an AI agent calls a model through the proxy, it can send its Shield360 agent token (Govern → Agent access) so the call is decided by its agent authorization profile: the approved models, the environment’s approved-model list, the run context, the person it acts for, and its daily and monthly spend limits. The local proxy, the container and the hosted proxy all do this.
| Header | Meaning |
|---|---|
X-Shield360-Agent-Token | The agent’s token (s360a_…). It marks the request as the agent’s. |
X-Shield360-On-Behalf-Of | Optional. The person the agent acts for. Without it, X-Shield360-User is used. |
X-Shield360-Environment | Optional. The run context, such as production. |
curl https://proxy.shield360.ai/t/$SHIELD360_PROXY_KEY/openai/v1/chat/completions \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "X-Shield360-Agent-Token: $SHIELD360_AGENT_TOKEN" \ -H "X-Shield360-Environment: production" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "max_tokens": 800, "messages": [{ "role": "user", "content": "Draft the release notes." }] }'After the budget check, the proxy asks Shield360’s check API with the agent’s token, the model, the provider, the prompt’s estimated input tokens and the request’s output limit (max_tokens, max_completion_tokens, max_output_tokens or Gemini’s maxOutputTokens). The proxy also proves which environment it enforces for: its collector token, or for the hosted proxy the service secret and the proxy key in the base URL. Shield360 refuses an agent token from any other environment.
| Decision | Proxy |
|---|---|
| Allow | Forwards. The call is counted against the agent’s spend limits. |
| Deny | 403 with code agent_not_authorized, the reason and the decision id. |
| Needs approval | 403 with code agent_needs_approval. The agent can ask for the scope through POST /api/agent-authorization/request-access. |
| Too many checks | 429 with code rate_limited and Retry-After. |
The proxy checks every allowed call again, because each one is counted against the spend limits and a kill switch must stop the next call. It reuses a refusal for 30 seconds for the same agent, model, provider, environment and person. A request with a token that is not an agent token is refused. So is an agent’s request when the proxy cannot tell which model it is for, unless it is a GET such as listing models.
If Shield360 can’t be reached, the proxy follows the policy’s fail mode. When guardFailClosed is on and aiGuard is block, it refuses with agent_authorization_unavailable. Otherwise it forwards with X-Shield360-Agent-Warning on the response and records a warning. SHIELD360_AGENT_AUTHORIZATION=warn forwards every refusal with that warning instead (useful while you roll it out), and SHIELD360_AGENT_AUTHORIZATION=off turns the check off on this machine.
Refusals and warnings are reported as guard decisions named agent-authorization. Every answer is also in the agent’s decision log. The agent token and the two context headers are removed before the request is forwarded, and they are never logged.
What is never logged or stored
Section titled “What is never logged or stored”The proxy never logs or stores Authorization, x-api-key, api-key, x-goog-api-key or cookie headers, request or response bodies, query strings (Gemini keys travel there), or detected values. It passes credential headers through to the provider unchanged and strips hop-by-hop headers.
shield360 proxy serve [flags]| Flag | Description |
|---|---|
--listen | Address to listen on. Default 127.0.0.1:8787. An address other machines can reach is refused unless you also pass --allow-remote. |
--allow-remote | Allow a non-loopback --listen address. Requires an access token. |
--access-token-file | File that holds the access token clients must send. SHIELD360_PROXY_ACCESS_TOKEN also works. |
--upstream name=URL | Add an upstream or replace a default one. Repeatable. |
--upstream-vendor name=vendor | The AI tool id aiTools uses for an upstream. The defaults are openai, anthropic and google. For another upstream, the vendor id defaults to the upstream’s name. |
--config | YAML or JSON file with the same settings. Flags override it. |
--state-dir | Where the enrolment, policy cache and undelivered events are kept. Default <config dir>/proxy. |
--detect-ip | Also detect IP addresses. |
--max-body-mb | Largest request body accepted. Default 20. Larger bodies get 413. |
--max-scan-mb, --scan-timeout, --scan-concurrency | The scan limits. |
--log-file | Write the log to this file, rotated at 10 MB, instead of stderr. |
--url, --install-key-file | Enrol on start when the proxy isn’t enrolled yet, and enrol again after a 401. SHIELD360_URL and SHIELD360_INSTALL_KEY also work. |
A config file:
listen: 127.0.0.1:8787detectIP: falsemaxBodyMB: 20maxScanMB: 2scanTimeout: 500msaccessTokenFile: /etc/shield360/proxy-token # the token itself never goes hereupstreams: litellm: url: http://litellm:4000 vendor: openai azure: url: https://my-resource.openai.azure.comenroll
Section titled “enroll”SHIELD360_INSTALL_KEY=<key> shield360 proxy enroll --url <shield360-url> [--install-key-file <path>] [--state-dir <dir>]status
Section titled “status”shield360 proxy status [--state-dir <dir>]Shows the service, whether the proxy is running, its enrolment, the policy in force and when it was fetched, its upstreams and counters. See Run it as a service.
The state directory defaults to <config dir>/proxy. SHIELD360_PROXY_STATE_DIR changes it.
Limits
Section titled “Limits”- API traffic only. The proxy sees requests that are sent to it. Browser traffic to ChatGPT, Claude or Gemini is covered by the browser extension.
- No TLS interception yet. A forward proxy that desktop apps and CLIs use without changing their configuration, routed with a PAC file and a per-deployment CA, is the next phase.
- One identity per proxy. Events name the enrolled user unless the app sets
X-Shield360-User. Per-user proxy authentication is planned. - JSON bodies only. Multipart uploads (audio, files) and bodies in an encoding other than gzip are not inspected. With prompt protection or prompt-injection checks on, a request with an unsupported
Content-Encodingis refused with415. - Large prompts are not inspected. Prompts over the scan limits are refused when prompt protection blocks, and otherwise forwarded and recorded as not inspected.
- No published image yet. Build the container from the repository. Shield360 SaaS customers can use the hosted proxy instead.