Letting Untrusted AI Agents Use Credentials They Can Never Read Declaw has designed a credential vault that keeps secrets out of AI agent sandboxes by storing them server-side and injecting them at an egress proxy, preventing prompt-injected or compromised agents from leaking credentials. The system uses per-sandbox ephemeral CAs and proxy-based credential attachment to ensure secrets never enter the guest VM, even when the agent is fully compromised. Letting Untrusted AI Agents Use Credentials They Can Never Read 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 /blog/how-to-give-an-agent-code-execution-without-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 REQUESTS CA BUNDLE → /etc/ssl/certs/ca-certificates.crt CURL CA BUNDLE → /etc/ssl/certs/ca-certificates.crt 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 secret Values go into OpenBao under a KV-v2 mount, one namespace per team — tenant isolation at the store, not just in a query filter. Our own Postgres holds only metadata and the per-domain injection rules; there is no value column anywhere in it. If the metadata write fails after the vault write, we roll the vault write back, so a half-created secret never leaves an orphan. You don't even have to store the secret with us. A secret can instead be a connector descriptor — a small JSON pointer into a store you already run: {"provider": "aws sm", "secret id": "prod/openai", "region": "us-east-1"} On a cache miss the worker reads the live value from your store, uses it for that one request, and never persists it. Eleven stores are wired: AWS Secrets Manager and SSM Parameter Store, GCP Secret Manager, Azure Key Vault, HashiCorp Vault / OpenBao, Infisical, Doppler, Kubernetes Secrets, 1Password Connect, CyberArk Conjur, and Akeyless. The connectors reuse the same credential-minting machinery as the brokered injection types below, so a connector can authenticate to your store with workload identity instead of a stored key. For the common case there's a preset catalog ~39 providers : you pick the provider and paste the key, and the destination host, injection format, and provider quirks are filled in for you — the version header one API requires, the Token vs Key scheme word another wants, the fixed basic-auth username a third pins. Presets expand server-side into ordinary scopes, so they're sugar over the same machinery, not a separate code path. Attaching the credential The static types are the boring-but-essential ones: Authorization: Bearer