Shadow AI capture

Shadow AI is any LLM call made by a managed workload that leaves the corporate network on its way to OpenAI, Anthropic, Azure OpenAI, or another model provider, without governance in the path. The Asqav shadow-AI shim co-locates a signing proxy next to your existing forward / egress proxy. Every outbound LLM request routed through the shim gets a signed receipt before it leaves the network, the existing proxy forwards the traffic unchanged, and the calling workload needs no SDK changes beyond an environment variable.

Asqav does not detect or discover agents. It signs and proves the actions of the agents you register. An unregistered agent becomes visible only where signing is enforced at an egress chokepoint, where unsigned LLM traffic is blocked or logged.

Receipts from this pattern pair with build provenance: an agent you govern leaves a signed record that can bind to its build artifact, which is exactly what an undeclared shadow agent cannot produce. For the strategy behind that asymmetry, read The unsigned agent is the incident on the blog.

Deployment

The shim is the only new network hop. It signs locally then forwards the original request upstream. Sign-only, fail-open: if the signer is unreachable the request still goes through and the failure is logged. Flip this to fail-closed by setting ASQAV_FAIL_OPEN=0 in the shim env.

Quickstart

Install the SDK and use the asqav shadow-ai CLI to scaffold, start, and probe the stack. Docker and Docker Compose are required on the host that runs the shim and signer.

bash
pip install "asqav[cli]"

# Scaffold docker-compose.yml, .env.template, and README.md into ./shadow-ai
asqav shadow-ai init

# Fill in POSTGRES_PASSWORD and ASQAV_API_KEY in ./shadow-ai/.env
cp ./shadow-ai/.env.template ./shadow-ai/.env

# Start the shim + signer stack via docker compose up -d --build
asqav shadow-ai up

# Probe shim /_healthz and signer /api/v1/health/; exits 0 only if both are healthy
asqav shadow-ai status

Then change one env var in the calling workload so its existing OpenAI or Anthropic SDK speaks to the shim instead of going direct:

bash
OPENAI_BASE_URL=http://asqav-egress-shim:8080/v1
ANTHROPIC_BASE_URL=http://asqav-egress-shim:8080

The shim preserves request and response bytes verbatim, so the OpenAI and Anthropic SDKs work unmodified. Each captured call emits a receipt with action_type="llm:egress" and capture_topology="network_proxy". Only a SHA-256 of the body is sent to the signer, never the raw body. Tail the stack with asqav shadow-ai logs --follow, and tear it down with asqav shadow-ai down.

Out of scope: personal-device browser usage

This deployment does not see traffic from an employee's personal phone, a personal laptop on a guest SSID, or a BYOD device that bypasses the corporate egress path. Catching browser-based shadow AI on unmanaged endpoints is a DLP / CASB problem, not a signing-proxy problem. Pair this pattern with a managed-device DLP solution (Netskope, Zscaler, Island, Cloudflare Gateway) if browser coverage is part of the scope. The Asqav signer only sees what flows through the proxy.

Verification

After the stack is up, send a test prompt through the workload and confirm a receipt was emitted. The signature record is also visible in the Asqav dashboard under Signatures.

bash
curl -H "X-API-Key: $ASQAV_API_KEY" \
  "http://localhost:8000/api/v1/export/json?start_date=$(date +%F)"

The newest record in the signatures array should have action_type == "llm:egress", an agent_id and signed_at, and a signed payload carrying capture_topology == "network_proxy" and the body hash. The dashboard surfaces the network_proxy topology under a Shadow AI label.

Reconcile registered agents against observed activity

Detection-category tools (inventory scanners, network monitors, eBPF observers) tell you what is running. Receipts prove what the governed set did. The gap between the two lists is where an undeclared agent lives. Asqav does not detect unregistered agents, but it gives you a provable governed set to diff against. Three steps, runnable from any shell with your API key.

First, pull the registered-agent inventory. This is your governed denominator:

bash
curl "https://api.asqav.com/api/v1/agents?revoked=false&suspended=false" \
  -H "X-API-Key: sk_live_your_key"

Second, export the signed activity for the window you are reconciling. The CSV carries agent_id, action_type, and signed_at per row (a JSON variant lives at /api/v1/export/json, and the dashboard shows the same records under Signatures):

bash
curl "https://api.asqav.com/api/v1/export/csv?start_date=2026-06-01" \
  -H "X-API-Key: sk_live_your_key" -o asqav_audit.csv

Third, compare both against your own egress logs (proxy, firewall, or SIEM). Three outcomes matter:

The receipts side of this diff is verifiable by a third party: each record carries a signature an auditor can check without trusting your logging pipeline.

Security model

Sign-only, body never relayed

The shim hashes the request body with SHA-256 locally and sends only the hex digest to the signer. Raw prompts, raw model outputs, and raw tool-call payloads stay inside your network. The signer follows the same trust contract as the self-hosted signer.

Fail-open by default, fail-closed by env var

If the signer call fails the request still goes through and the failure is logged. Set ASQAV_FAIL_OPEN=0 in the shim env to flip to fail-closed, which blocks the upstream call when the signer is unreachable. Pick the mode that matches your operational risk tolerance.

No MITM on the upstream

The shim does not break TLS to a third-party upstream. It terminates the workload's connection to the shim, signs locally, and opens its own outbound HTTPS connection to the model provider. The upstream sees a normal TLS handshake from your egress IP.

Related