Letting Untrusted AI Agents Use Credentials They Can Never Read | Declaw<br>Back to blog2026-07-07Security<br>Letting Untrusted AI Agents Use Credentials They Can Never Read<br>The full design of Declaw's credential vault: how a secret stays out of the sandbox, gets attached at the egress proxy on the way out, and why a fully compromised, prompt-injected agent still leaks nothing but a placeholder.
An AI agent is only useful once you give it secrets — an LLM API key, a database password, a token for an internal service. The usual way to hand one over is an environment variable in the sandbox where the agent runs. That's also the problem: the sandbox is exactly where untrusted, model-driven code executes. A prompt-injected agent — or a compromised dependency — can read its own environment, dump /proc, walk the process table, and your key walks out with it. You've handed the keys to the one component you've already decided not to trust, and sandboxing doesn't change that: a good sandbox bounds the blast radius of the code, but the secret is sitting inside that radius with it.
We covered the practical side — store a secret, scope it, call an API from an agent that never holds the key — in How to Give an AI Agent Code Execution Without Handing Over Your Credentials. This post is the other half: the whole design, end to end — store, injection, isolation — and why a fully compromised agent leaks nothing but a placeholder.
So we stopped trying to find a better hiding spot inside the box, and made the secret never enter the box at all.
The shape
Three moving parts:
The secret value lives server-side in a vault (we use OpenBao). It's write-only: you store it once, and no API ever returns it again — not to you, not to the agent.
The sandbox is created with a reference — just the secret's name. Inside the VM, the environment variable holds a placeholder string, declaw:vault-managed.
Every sandbox's outbound traffic already routes through an egress proxy. When the agent makes a request to a host you've scoped the secret to, the proxy attaches the real credential — after the request has left the VM.
The agent uses the credential against the destination you intend, and never holds it. A prompt-injected agent that dumps its whole environment leaks a placeholder.
What makes this a real boundary and not a demo is where the proxy runs. The agent runs in a Firecracker microVM. The proxy runs in that sandbox's host-side network namespace — outside the guest, across the virtualization boundary — and it is the VM's only route to the network. The secret lives in the proxy's memory for exactly one request and is never written into the guest: not a file, not an env var, not a log line.
How the VM trusts the proxy's certs
The first question any engineer asks: if the proxy terminates TLS to inject into an HTTPS request, why doesn't the agent's client reject the substituted certificate?
Each sandbox gets its own ephemeral CA — a per-sandbox ECDSA P-256 key whose private key never leaves the proxy's memory and is never written to disk. For each upstream host the agent contacts, the proxy mints a short-lived leaf signed by that CA.
For the guest to accept those leaves, the CA's public cert is installed into the guest's trust stores at boot — appended to the system bundle and exported through the variables every major runtime honors:
SSL_CERT_FILE → /etc/ssl/certs/ca-certificates.crt<br>REQUESTS_CA_BUNDLE → /etc/ssl/certs/ca-certificates.crt<br>CURL_CA_BUNDLE → /etc/ssl/certs/ca-certificates.crt<br>NODE_EXTRA_CA_CERTS → /usr/local/share/ca-certificates/declaw-sandbox.crt
So OpenSSL, Python requests, curl, and Node all chain-validate the proxy's leaf with no per-app configuration. The CA is per-sandbox, so one sandbox's CA is worthless against another's traffic.
One limitation falls out of the same mechanism: a client that pins a specific upstream certificate — rather than trusting the system store — will reject the proxy's minted leaf, so those endpoints can't be brokered. It's rare in server-to-server API calls, but it's real.
Injection fires on domain match, not on the env var. The placeholder is a courtesy so the agent's SDK has a value to read; the proxy injects into any request to a scoped host, whether or not the agent ever touched the variable — and it uses Set, so it also overrides any header the agent tried to supply itself.
Scoping, and why it's anchored
A secret is attached only to hosts that match one of its scopes. A scope's domain pattern is an exact host, a *.-wildcard, or a ~-prefixed regex. Matching runs on RE2 — linear-time, no catastrophic backtracking — and every scope's pattern is anchored to ^(?:…)$, so a credential scoped to api.example.com can't be coaxed onto api.example.com.evil.com. Anchoring is enforced rather than advisory: the pattern is wrapped (so even a top-level alternation a|b anchors correctly as ^(?:a|b)$), and it applies to hand-written custom scopes, not just the built-in presets.
Storing the...