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.
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:
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.
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:
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):
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:
- A flow to a model provider with no matching receipt in the window: traffic that bypassed the governed path. Behind the chokepoint above, this is a deny-rule hit or a sign failure in the shim's own log, not a silent success.
- A registered agent with no receipts in the window: a workload that went quiet. It may be decommissioned, or it may have been repointed away from the shim.
- An inventory entry from your detection tooling that maps to no registered agent: an unregistered workload making model calls, the case to escalate.
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
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.
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.
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
- Self-hosted Signer - the signer container this compose re-uses and the env contract for keys and storage.
- CLI Reference - full flag reference for
asqav shadow-ai init / up / down / status / logs. - Spec (IETF) - the receipt format the shim emits, including the
llm:egressaction_type vocabulary. - MCP Security - companion pattern for signing tool invocations inside an MCP server.
- Build Provenance - bind each governed action to the build that performed it via the executable-hash and SBOM 4-tuple.
- The unsigned agent is the incident - why enforced egress signing makes unsigned activity visible.