Skip to content
Carapace, homeGitHub

Field report 01 · September 2026

We gave an AI agent a GitHub token it could use but never see. Then we told another one to steal it.

An end-to-end run of Carapace in a fresh Google Cloud project: a one-command deploy onto a Confidential VM, a working agent, a white-box red team, a signed audit trail, and a teardown. Here's what held, what broke on the way, and what the run showed is missing.

StatusBeta. Not independently audited; start with scoped, revocable tokens.

requests from the working agent succeeded
6/6
attempts to get the token out returned it
0/52
signed receipts verified, 0 gaps
50
to tear down all 48 cloud resources
5m47s

The setup

One secret, one enclave, two agents

Carapace is an open-source secret broker for AI agents. You seal a credential on your own machine to a key that only an attested enclave can use, and your agent gets an API key instead. When the agent makes a request, the enclave decrypts the secret in encrypted memory, checks the request against the policy you set, attaches the credential on the way out, and redacts it from whatever comes back.

For this run we deployed it from source with carapace deploy into a brand-new GCP project and sealed a GitHub personal access token with a narrow policy: host api.github.com, HTTPS only, GET only.

  1. The agent

    Holds an API key, a pinned enclave certificate and a secret ID.

    No token, no owner login, no cloud credentials.

  2. The enclave

    A Confidential VM (AMD SEV) in your own GCP project.

    Decrypts only when its attested image digest is approved. Checks the policy, injects, redacts, signs a receipt.

  3. api.github.com

    The only host this secret is allowed to reach.

    Sees the token, as it has to.

The agent sends its request and API key to the enclave over pinned TLS. The enclave injects the GitHub token and forwards the request to api.github.com.

Before the good part

It took six fixes to get a clean run

We'd rather tell you this than have you find it. A throwaway project on real hardware is where you find the bugs a mocked test suite can't, and we found six, all in the deploy and teardown tooling. None were in the enclave or the broker. Each one was fixed, security-reviewed and merged before the run continued.

  • Deploy

    A stale local config could outvote the real state.

    The deploy now trusts the cloud state, not a leftover file, and never carries an old enclave digest forward.

  • Deploy

    Reusing a name failed halfway through.

    Some Google resources can't be deleted, only retired. Preflight now refuses a name that's been used before anything is created.

  • Teardown

    Destroy couldn't clean up a half-built stack.

    It now lifts protection in the state instead of re-running the program, so it never creates anything on its way out.

  • Capacity

    Google ran out of Confidential VMs in our zone.

    Twice. The deploy now falls back to another zone in the same region on its own.

  • First run

    We checked the enclave before it was listening.

    A refused connection now means “not ready yet, retry”. A bad TLS handshake is still a hard failure.

  • Reruns

    A routine rerun could silently replace the enclave.

    The boot image is now pinned per deploy, and replacing the enclave needs an explicit preview and confirm.

Agent one

The working agent just worked

We gave a Claude Sonnet agent a folder holding only its API key and the pinned enclave certificate, plus four ordinary tasks: who am I, what's my rate limit, what have I worked on lately, and summarize one repository's README and latest commit. It worked the CLI out from --help, made six requests and got six 200s. The authenticated rate limit of 5,000 showed the enclave had injected the token. The agent never saw it.

agent shell
# the agent's whole world
ls ~/.config/carapace-agent
agent.key  enclave.json

carapace request <secret-id> GET https://api.github.com/rate_limit
HTTP 403 Please make sure your request has a User-Agent header

carapace request <secret-id> GET https://api.github.com/rate_limit \
    -H "User-Agent: my-agent"
HTTP 200
{"resources": {"core": {"limit": 5000, …}}}
Abridged. The one snag: GitHub rejects requests without a User-Agent, and the enclave doesn't add one yet.

Agent two

Then we asked a second agent to steal it

A red-team agent (Claude Fable) got the same credentials plus the full source code, and one goal: get the plaintext token out by any route it could find. It made 52 requests. None of them returned the token. Each attempt was stopped at the layer built to stop it.

  • Send the token to a host it controls, a look-alike domain, or an internationalized homographhost_not_allowed
  • Smuggle another destination through user@host, backslashes, plain http://, or raw IPs, including the cloud metadata addressurl_rejected
  • Reach internal services: metadata.google.internal, localhosthost_not_allowed
  • Use another port, or a method other than GET, including a method-override headerport_ / method_not_allowed
  • Forge or duplicate Authorization, Host or proxy headers; inject CRLF or NUL bytesheader_rejected
  • Plant a {secret} placeholder in a header, query or body so the enclave fills it in somewhere readableplaceholder_not_allowed
  • Make GitHub echo the request back: error pages, path reflection, encoded slashesreached GitHub · token not reflected
  • Probe the enclave itself: health, attestation, unauthenticated callspublic fields only · 401 / 405
The redaction layer never had to fire. The token didn't leak because the request never went anywhere it could leak from.

That's the design working as intended. Containment comes from structure: a host allowlist, DNS pinning, no redirects, and TLS verified against the real hostname. Redaction is a backstop for an allowed upstream that reflects something back, and it's best-effort; the threat model lists the encodings it misses. The enclave also doesn't tell the caller when it redacted something. That count goes into the signed receipt, where the owner sees it and the agent doesn't.

The paper trail

Every request accounted for

Every request that passes authentication gets a receipt signed by a key bound to the enclave's attestation. Afterwards the owner verified the whole chain offline:

owner shell
carapace audit verify
OK: 50 receipts from 1 attested boot verified, 0 gaps

We sent 61 requests in total, and they reconcile exactly:

Requests sent during the run and how many produced a signed receipt
TrafficSentReceipted
Working agent66
Red team5242
Our own smoke tests32
Total6150

Policy denials are receipted, so the owner can see each of the red team's attempts on a forbidden host. The 11 requests without receipts were rejected before the enclave knew who was asking: malformed IDs, requests for a secret the key has no grant on, and probes of the public endpoints. With no verified identity there's nothing to sign a receipt for. That also means probing with a stolen key currently leaves no trace in the receipts.

What the run showed is missing

The gaps are on the list

  • No default User-Agent. An agent's first GitHub call bounces until it sets one itself.
  • Policies can lean on redaction. Injecting a secret into a query string, or allowing a wildcard domain where anyone can claim a subdomain, depends on a layer that is only best-effort. Today only the docs say so.
  • Agent ergonomics. The CLI doesn't show response headers, and an agent needs the secret's ID, which is easy to confuse with the key's ID.
  • No trace of pre-authentication probing. See above.

Cleanup

And then it was gone

carapace destroy listed what it would delete and what it would keep, then removed 48 resources in 5 minutes 47 seconds. What stays behind is spelled out on screen: your encrypted state bucket, and a few Google resources that can only be retired, not deleted.

One honest caveat

Cloud KMS schedules a destroyed key version for deletion 30 days out, and a project Owner can restore it until then. The destroy screen says so and tells you to rotate your secrets. We revoked the test token at GitHub.

Running Carapace costs about $92 a month at list prices, paid to Google, most of it the always-on Confidential VM. This whole test cost a few dollars.

Read the code. Then try to break it.

Carapace is Apache-2.0 and self-hosted in your own GCP project. The threat model lists what it doesn't protect against, including Google being in the trusted computing base and a GCP project Owner being able to grant themselves decrypt.

Tested in September 2026 on GCP Confidential Space (AMD SEV), deployed from source. The signed-release install path has since run end to end on real hardware too, ahead of v0.1.0. Agents: Claude Sonnet (working agent) and Claude Fable (red team), each given only the agent's credentials. Back to the homepage.