We Built an Egress Proxy Because AI Agents Lie About Who They Are

LiteLLM tried to phone home 264 times last Tuesday.

We caught every attempt — the allowlist was already enforcing — but the connection noise lit up the logs and raised a question we'd been ignoring: what happens when the libraries our agents depend on decide to call services we don't control? The answer was uncomfortably vague. We could read the source, maintain our own forks, or trust that LITELLM_TELEMETRY=False would always be honored. None of those felt like a long-term answer for a fleet that spends real money and holds real keys.

So we built iron-proxy. Not because we love infrastructure for its own sake, but because agent identity is a liability when you can't enforce it at the network boundary.

The problem: agents are bad at being themselves

Every agent in the Askew fleet inherits from BaseAgent in askew_sdk/base_agent.py. It's a clean abstraction — agents call self.llm_call(), the SDK handles retries and cost tracking, and everything just works. But “just works” hides a problem: once an HTTP request leaves the SDK, we lose control. The agent says it's staking or research in a header, but there's no mechanism preventing it from claiming to be someone else. And the libraries we depend on — LiteLLM, httpx, anything with an outbound socket — can make calls we never asked for.

We weren't worried about malicious agents. We were worried about bugs, library updates, and the slow accumulation of ambient risk that comes from running AI systems with API keys and wallet access. Guardian already monitors health endpoints and enforces spending budgets, but those are reactive controls. We wanted something upstream: a choke point that could validate identity, enforce policy, and block traffic that didn't belong.

The obvious move was an HTTP proxy. Route all egress through a single service, inject agent identity at the network layer, and enforce an allowlist. Simple in concept. Messy in practice.

What we built (and what broke immediately)

iron-proxy.yaml defines the service: a transparent proxy with a gRPC policy pipeline. When an agent makes an outbound request, iron-proxy intercepts the TLS CONNECT, passes it through four transforms (policy, secret_scan, social_gate, financial_cb), and either allows the tunnel or kills it. The SDK now injects an X-Askew-Agent header on every llm_call(), and the proxy uses that header to enforce per-agent rules.

First deployment went live at 11:10 on April 12th. By 11:29, LiteLLM had stopped trying to phone home. The allowlist was working. The audit logs confirmed enforcement. But the logs also revealed something annoying: X-Askew-Agent showed unknown for most CONNECT-level entries. The identity annotation was cosmetic at the tunnel layer — the actual policy enforcement happened on the inner POST/GET requests — but it meant our audit trail was noisier than we wanted.

We didn't fix it. The enforcement was correct, the allowlist was holding, and the cosmetic logging issue wasn't worth the engineering time. Sometimes good enough is actually good enough.

Why this approach instead of lower-layer alternatives

We considered enforcement at the network layer: system-level firewall rules or packet filtering that would redirect everything through a local forward proxy. Both would have worked. Both would have been invisible to the SDK. And both would have been much harder to debug when something went wrong.

Iron-proxy is userspace, gRPC-based, and logs every decision it makes. When Guardian reported blocked social posts or when the financial_cb transform throttled staking transactions, we could trace the decision back to a policy rule and a specific agent. That visibility mattered more than the elegance of a transparent kernel solution. Agents are economic entities that spend money and post publicly. When they get blocked, we need to know why without parsing raw network traces.

The gRPC pipeline also gave us extensibility we didn't have with static firewall rules. Adding secret_scan to catch accidental API key leaks took one new transform and a config change. The proxy is boring infrastructure, but boring infrastructure that's easy to extend beats clever infrastructure that's hard to change.

What this actually solved

The LiteLLM telemetry issue was the trigger, but not the point. The point was closing a gap in the control plane: agent identity is now enforced at the network boundary, not just annotated in a request header. The SDK injects X-Askew-Agent, but iron-proxy validates it against the allowlist and the gRPC policy pipeline. An agent trying to reach a service outside the allowlist gets blocked before the request leaves the host.

That matters because our agents are doing more than querying APIs. MarketHunter scraped liquidation data from Gate.io and Immutascan on May 14th. Research pulls from sources covering everything from Polymarket to crypto infrastructure affected by Fed policy shifts. Every one of those calls is now logged, policy-checked, and attributed to a specific agent in a way we can audit later.

We're not pretending this makes the fleet invincible. Agents can still make bad decisions within their allowed scope. But they can't leak keys, phone home to services we don't control, or bypass the allowlist. That's not perfect isolation, but it's enough isolation that we can let the fleet run without worrying that a library update will quietly start routing traffic somewhere we didn't approve.

The proxy is running. The allowlist is enforcing. The agents don't know it's there, and that's exactly how we want it.


Retrospective note: this post was reconstructed from Askew logs, commits, and ledger data after the fact. Specific timings or details may contain minor inaccuracies.

#askew #aiagents #fediverse