DEV Community

kh.vibecoding
kh.vibecoding

Posted on Originally published at habr.com AI-assisted

Your API Key Will Leak. Make That Not Matter

According to GitGuardian's State of Secrets Sprawl report, about 23.7 million secrets — API keys, tokens, passwords — leaked into public GitHub in 2024 alone. The overwhelming majority aren't the result of a hack; they're ordinary commits, logs, and client-side bundles. A fresh key that lands in a public repo gets tried by bots in under a minute.

The conclusion we reached after the Nth incident: fighting to keep a key from leaking is a war you lose. The winning move is to make the leak useless.

Why keys always leak

A single OpenAI key in an average project lives in five places at once:

  • in .env on developers' laptops;
  • in CI secrets (and sometimes in CI logs — a printenv in a debug step);
  • in the production config;
  • in "just for a minute" scripts (test_gpt.py with the key hardcoded as a string);
  • and, more recently, in the context of an AI agent that "writes the code itself."

Every copy is an independent point of failure. The classic countermeasures all work, but each one has a ceiling:

Measure What it covers What it doesn't cover
.env + .gitignore an accidental commit logs, CI, agents, "just a quick script"
Scanners (gitleaks, trufflehog) a leak into git before push every other channel
Secret managers (Vault, AWS Secrets Manager) storage and delivery the key still ends up in the app's runtime environment
Provider-scoped keys blast radius not every provider supports them; revoking one still breaks every consumer of that key

The common thread: in every scheme, the real key ends up in the consumer's hands at some point.

Which means it leaks along with it.

The scheme: credential proxy

The idea isn't new (it's how, say, payment tokenizers work), but for API keys it's barely used. Break the link between the app and the key:

app ──(vlt_pass)──▶ proxy ──(real key)──▶ provider
                       │
              status / IP / limits → log
Enter fullscreen mode Exit fullscreen mode

Secret — the real key. Entered once, encrypted, and never returned by any API, ever.

Pass — a virtual token bound to a secret. Its own IP binding, its own rpm/rpd limits, its own lifetime, its own log. This is what applications, scripts, and agents actually get.

Proxy — validates the pass, decrypts the secret in memory for the duration of a single request, substitutes it, and streams the response back.

Consequences:

  • The zone where the original key can leak shrinks down to a single server.
  • A leaked pass isn't an incident: wrong IP → 403, over the limit → 429, revocation is one click, the original is never touched.
  • Side bonus: you can see who is calling each downstream service and how much, because every consumer has its own pass.

For your code, the switch is two lines: api.openai.com<proxy>/p/openai, sk-…vlt_…. The path, body, headers, and SSE streaming all pass through unchanged.

How it works under the hood (our implementation)

We built this scheme as a service — ProxyKey (Node 22 + Fastify 5 + PostgreSQL + Redis). A few technical decisions that might be interesting independent of the product:

  • Envelope encryption: every secret gets its own DEK (AES-256-GCM, AAD = secret id), and the DEK is wrapped by a KEK from the environment. Decryption happens in memory for the duration of a single request; the plaintext is never cached or logged anywhere, and the DEK is zeroed out after use.
  • Tokens aren't stored: the database only holds SHA-256 hashes of passes; the hot path validates against a Redis cache (TTL 300s) with a fallback to Postgres.
  • Degradation is asymmetric: if Postgres goes down, we serve from cache (fail-closed for anything not cached); if Redis goes down, we validate against Postgres, but rate limits fail open — validation does not.
  • SSRF guard: custom base URLs are resolved and checked against private/metadata IP ranges — otherwise a proxy that accepts user-supplied upstreams is a ready-made SSRF vector.
  • Request logs: Postgres partitioned by month, metadata only, no authorization headers or key values.
  • Telegram bots are a special pain: the bot token lives in the URL path, and aiogram/grammY (popular Telegram bot libraries) validate its format before the first request. The proxy accepts bot<digits>:vlt_… and ignores the digits — the format passes client-library validation, and the real token gets substituted server-side.

AI agents: the main new consumer of keys

This is the actual reason we got into this in the first place. Agents (Claude Code, Cursor, and the like) spin up services and configure bots — they constantly need keys. But anything that ends up in a model's context has to be treated as published: context gets logged, traced, and can be extracted via prompt injection.

The fix is to give the agent a tool, not a secret: an MCP server for the vault. The agent connects via a URL with an mcp_… token and can issue, rotate, revoke passes, and read logs. "Read the real key" is simply not in the tool set — that's a property of the access protocol, not a promise we're trusting the agent to keep.

Our favorite scenario is the "pending secret": an agent deploys a Telegram bot whose token doesn't exist yet. The agent creates a pending pass, wires it into the config, and hands a human a link; the human enters the real token in the panel, the pass activates, and the bot comes alive. The agent finished the job without ever seeing the secret.

Honest trade-offs

  • The proxy is a critical dependency. If it's down, all your calls are down. Replicas and validation caching help, but you need to understand this going in.
  • The proxy sees the traffic. The question isn't whether it sees it, it's what it logs. In our implementation, only metadata; body previews are optional and opt-in per pass. If your threat model doesn't allow a third party in the loop, run this pattern yourself — it's reproducible.
  • Latency. One extra hop plus a cache lookup adds single-digit milliseconds against hundreds of milliseconds of LLM generation. For low-latency, non-LLM APIs, do your own math.

Disclaimer

We're the people behind ProxyKey (proxykey.org). The service is free, no card required; the panel, the proxy, and the MCP server are described here honestly, limitations included. The credential-proxy pattern is reproducible without us — if you build your own, just don't skip the SSRF guard and the fail-closed validation path.


Built at Hikmah Labs, a small studio I run. ProxyKey itself lives at proxykey.org.

Top comments (0)