Files
bot-bottle/docs/research/agent-credential-proxy-landscape.md
T
didericis-codex cdb1870b1c
test / unit (pull_request) Successful in 29s
test / integration (pull_request) Successful in 43s
docs(agent): clarify claude oauth env
2026-05-28 18:20:09 -04:00

23 KiB

Agent credential proxy landscape

Consolidated research on running an auth-header-injecting proxy in front of an AI agent so API tokens stay out of the agent's process space. Folds in the per-service mechanics for the Anthropic OAuth token and the Gitea PAT — the two cases bot-bottle hits first — and surveys existing tools as of May 2026.

Companion to secret-minimization-over-dlp.md (the architectural framing — why this matters), and to local-vs-remote-agent-execution.md (the broader threat model that flagged long-lived static tokens as the biggest credential risk).

Summary

Today every bot-bottle agent gets CLAUDE_CODE_OAUTH_TOKEN (and any bottle.env secrets like a Gitea PAT) injected as env vars, which means the agent process can read them with printenv or /proc/self/environ. A prompt-injected or hijacked agent can ship those bytes to any allowed host. Linux has no primitive for "this env var exists in my process but I can't read it" — the only credible boundary is to put the credential in a different process that the agent cannot read, and let the agent talk to it over a narrow API. Default Docker enforces that boundary at the kernel level via ptrace_may_access; a future smolmachines backend enforces it harder, at the VM line.

Several existing tools implement this pattern, but none of them are a clean drop-in for bot-bottle today: the most architecturally aligned (nono) is alpha; the most mature open-source (Infisical Agent Vault) requires TLS MITM and would double up on pipelock's TLS-interception stack. For the Anthropic-token slice, a small bot-bottle-specific reverse proxy modeled on the phantom-token shape is probably the right call. For Gitea / GitHub / GitLab, the same proxy generalizes by config.

The shared problem

Linux has no per-env-var ACL. Once a var is in a process's environ, the process and its descendants own it. The deeper boundary is process-level: hold the credential in a process the agent cannot read.

Default Docker enforces that boundary for you. The kernel's ptrace_may_access check rejects /proc/<pid>/environ reads when the caller's UID/GID don't match the target's and the caller lacks CAP_SYS_PTRACE or CAP_PERFMON. A node-uid claude attempting to read a root-owned proxy's environ gets EACCES. Escape hatches (--cap-add=SYS_PTRACE, --cap-add=PERFMON, --privileged) are not used by bot-bottle. Yama ptrace_scope is irrelevant — it only relaxes the same-UID relationship check; the cross-UID match requirement still blocks the read. On a smolmachines backend the boundary becomes the VM line; same property, harder.

claude-code's apiKeyHelper setting is not a boundary. The helper is invoked by claude's own process, so claude can just call it via Bash and capture stdout. Same trust domain.

The remaining credible designs reduce to three:

  • Header-injecting reverse proxy — agent points at a localhost URL; proxy holds the credential; proxy adds the auth header and forwards. Cleanest fit for services that support a BASE_URL-style override (Anthropic, OpenAI, Portkey, etc.).
  • Forward proxy with TLS termination — agent keeps the real service URL; an HTTPS_PROXY MITM intercepts, terminates TLS with a container-local CA, injects the header, re-encrypts. Heavier; required when the agent's tool can't be pointed at an explicit URL.
  • Don't ship the token at all — fall back to per-session login or short-lived child tokens. Operationally heavier; the long-lived OAuth token was chosen precisely because it's portable (Keychain on macOS, file on Linux).

Per-service mechanics

Anthropic / Claude Code

Today's wiring (bot_bottle/cli/start.py): the host's BOT_BOTTLE_CLAUDE_OAUTH_TOKEN is forwarded into the bottle as CLAUDE_CODE_OAUTH_TOKEN via docker run -e CLAUDE_CODE_OAUTH_TOKEN (no =value, so the value never lands on argv — good). Inside the bottle, claude runs as node (UID 1000) with --dangerously-skip-permissions. Its Bash tool can do printenv CLAUDE_CODE_OAUTH_TOKEN, cat /proc/self/environ, node -e 'console.log(process.env)' and capture the value into the conversation. The DLP / egress story (secret-minimization-over-dlp.md) explains why scanning on the way out doesn't save you here.

Routing primitive: ANTHROPIC_BASE_URL is documented as a generic proxy/gateway override, not just Bedrock/Vertex, and works alongside bearer auth. The proxy sets Authorization: Bearer $TOKEN and forwards to https://api.anthropic.com. Claude as node only sees the URL, never the token.

Confirmed gotchas:

  • SSE streaming: the proxy must not buffer responses (nginx proxy_buffering off, or a streaming-aware proxy). Claude Code uses SSE only — no websockets.

  • Forward anthropic-version, anthropic-beta, and X-Claude-Code-Session-Id untouched. Stripping them breaks tool use / extended thinking / session aggregation.

  • GitHub issue #36998: interactive mode historically bypassed ANTHROPIC_BASE_URL for some startup calls (auth validation / org lookup), connecting directly to api.anthropic.com. Marked closed but verify with tcpdump or strace -e connect against the pinned claude-code build before trusting the isolation.

  • Tool search (ENABLE_TOOL_SEARCH) is disabled by default when ANTHROPIC_BASE_URL is non-Anthropic; re-enable explicitly if needed.

  • Out-of-band outbound traffic does not route through ANTHROPIC_BASE_URL:

    • statsig.anthropic.com — telemetry (disable: CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1, DISABLE_TELEMETRY=1)
    • Sentry error reporting (disable: DISABLE_ERROR_REPORTING=1)
    • registry.npmjs.org, github.com, release-assets.githubusercontent.com — MCP installs + autoupdater
    • pypi.org, bun.sh — if the Bash tool installs Python or Bun packages during a session

    A hijacked claude could exfil the captured token (or any other data) through any of these even with the proxy in place. Pair the proxy with an explicit egress allowlist for the full benefit (bot-bottle does this via pipelock).

  • Token refresh: claude setup-token issues a ~1-year OAuth token with no client-side refresh, so a static proxy value is fine. The flip side is a one-year blast radius if the token leaks — see claude-code-token-revocation.md.

  • No request signing / anti-replay on the Messages API; header rewriting is safe.

  • --bare mode reads only ANTHROPIC_API_KEY, not CLAUDE_CODE_OAUTH_TOKEN. Not relevant to the interactive flow bot-bottle ships, but worth noting if --bare is ever wired in.

Gitea (tea + git HTTPS)

Token sources, in precedence order:

  1. GITEA_SERVER_TOKEN env var — registered via cli.EnvVars("GITEA_SERVER_TOKEN") in cmd/login/add.go.
  2. ~/.config/tea/config.yml (XDG) or ~/.tea/tea.yml (legacy fallback) — plaintext YAML, token field under the login entry.
  3. OS credstore — OAuth logins only; PAT-based logins go to the YAML file.

There is no credential.helper analogue: no --token-file, no FD-passing, no socket-based credential protocol. So the token can't be hidden inside tea's process — it has to be held by a different process the agent cannot read. For HTTPS git operations, tea uses go-git directly with BasicAuth{Username: token, Password: ""} (modules/git/auth.go), bypassing git's own credential.helper machinery. A credential-helper shim alone won't intercept tea repo clone — the proxy has to sit on the HTTP path itself.

Header form: use Authorization: token <…>, not Bearer. go-gitea/gitea#16734 emitted spurious "missing CSRF token" errors for Bearer on some endpoints. The fix landed upstream, but token has always been the header-safe choice.

No CSRF / no per-request nonce on the Gitea API for token auth, so a header-rewriting proxy is safe.

Plain git push from claude can use either the proxy (rewritten remote URL) or a credential-helper shim that calls the proxy. The rewritten-remote approach keeps the token bytes out of git's credential negotiation entirely. (Note: this is parallel to the existing git-gate in PRD 0008, which solves the SSH-push case via a per-bottle mirror.)

GitHub / GitLab

Structurally identical to Gitea for PAT auth: stateless Authorization: Bearer <…> (GitHub PATs and GitLab PATs both accept Bearer, and GitHub also accepts token <…> for legacy clients), no CSRF, no signing. Per-route allowlisting at the proxy is the lever for narrowing blast radius. GitHub fine-grained PATs and GitLab project-access tokens are the issuance-side mitigation. Either composes cleanly with the same proxy.

Proxy architectures

Four shapes worth comparing. The first is the lowest-friction match for bot-bottle today.

Shape Pros Cons
In-container reverse proxy (recommended) Self-contained per agent, no host changes, no MITM CA, no Go-loopback workaround. Works for any service with a BASE_URL-style override (Anthropic, OpenAI, Portkey). Doesn't work for services that hardcode the upstream URL — requires either rewriting the client config or moving up to a forward proxy.
In-container forward proxy + TLS termination Transparent to the agent's tooling — every HTTPS request gets intercepted regardless of base-URL support. Needs a container-local CA in the trust store (same machinery PRD 0006 set up for pipelock). Has the golang/go#28866 loopback gotcha: net/http ignores HTTPS_PROXY when set to 127.0.0.1/localhost, so the proxy must bind on a non-loopback address (Docker bridge IP, host.docker.internal, or ip addr add 10.0.0.1/32 dev lo).
Host-side proxy Token stays entirely outside the Linux VM. This is the Docker AI Sandbox shape. A host daemon to maintain; the published port is reachable by any container on the host unless firewalled. UDS-across-VM doesn't work on Docker Desktop on macOS (no AF_UNIX connect() over the VM), but host.docker.internal:<port> over TCP works fine.
Sidecar container Clean isolation; portable across hosts. Matches the existing pipelock / ssh-gate / git-gate topology. Another container to orchestrate per agent; the token is in another container's env, which is a lateral move unless the sidecar runs with stricter isolation than the agent container does.

For bot-bottle today — local Docker, per-agent containers, the root-owned-helper pattern already established by the SSH agent — the in-container reverse proxy is the lowest-friction option that gives the desired property. The sidecar-container shape is the natural evolution if the proxy needs the same per-bottle isolation that pipelock has.

Landscape of existing tools (May 2026)

Two categories:

  • A. Generic LLM / API gateways that happen to support credential injection as a side feature.
  • B. Purpose-built agent credential brokers — newer, closer to what bot-bottle wants.
Tool Category License Topology Injection mechanism ANTHROPIC_BASE_URL compatible Per-route allowlist Maturity
Docker AI Sandboxes B Proprietary Host-side proxy Header overwrite, OS keychain No (intercepts by domain) Domain only GA (Mar 2026)
Cloudflare Sandbox Auth B Proprietary Sandbox sidecar + ephemeral CA TLS intercept + Outbound Worker No (platform-specific) Host/IP/method GA (Apr 2026)
Infisical Agent Vault B MIT (EE carve-out) In-process HTTPS_PROXY forward proxy TLS MITM, dummy-to-real swap No — HTTPS_PROXY model Service-level Active; v0.19.0 May 2026, ~1k
nono B Apache-2.0 In-process reverse proxy Phantom token, explicit URL routing YesBASE_URL=http://127.0.0.1:PORT/… Host + endpoint Early alpha; v0.53.0 May 2026, 2.4k
Aegis B Apache-2.0 In-process reverse proxy Path routing (localhost:3100/{svc}/…) Configurable, undocumented for Anthropic Method/path/rate/time Very new, 10
OneCLI B Apache-2.0 Reverse proxy + management UI Host/path matching, Bitwarden integration Configurable Per-agent scoping Active; v1.23.0 May 2026, 2.1k
Aembit B Proprietary Sidecar + cloud control plane TLS intercept, SPIFFE, JIT creds No — intercepts by destination Policy-based GA (Apr 2026)
LiteLLM Proxy A MIT Reverse proxy Virtual key → upstream key Yes — set base URL to LiteLLM Route-level 45k; CVE-2026-42208 exploited Apr 2026, patch v1.83.7
Portkey Gateway A MIT (OSS core) Reverse proxy Virtual key vault (cloud or Enterprise self-host) Yes — documented for Claude Code Config-based Production; virtual-key vault needs Enterprise for self-host
Helicone A Apache-2.0 Reverse proxy Proxy header auth; agent still holds own key Yes No Maintenance mode (Mintlify acq. Mar 2026)
LangSmith LLM Auth Proxy A OSS Helm Envoy sidecar JWT + ext_authz upstream key injection Yes URL allowlist Enterprise (LangSmith ≥ v0.13.33)
Kong AI Gateway A Apache-2.0 Reverse proxy Plugin per-route/consumer Yes Plugin-level Production, heavy
AWS IMDSv2 n/a Link-local Per-instance metadata n/a n/a Conceptual analog only

Cluster commentary

  • The phantom-token pattern (nono) is the cleanest architectural fit for bot-bottle. The agent receives a per-session cryptographically random token scoped to the localhost proxy; the proxy validates and swaps for the real upstream credential. No TLS interception, no CA trust setup, works directly with ANTHROPIC_BASE_URL. Blocker: nono is explicitly "early alpha, not security audited."

  • TLS-MITM forward proxies (Infisical Agent Vault, Cloudflare Sandbox Auth, Aembit, the existing pipelock) all double up on the CA-trust machinery PRD 0006 already built for pipelock. Adopting Agent Vault would mean two MITM proxies in each bottle unless one is dropped. Also subject to golang/go#28866 — must bind on a non-loopback address.

  • LLM gateways (LiteLLM, Portkey, Helicone, Kong) all support credential injection but are built for cost / observability / fallback, not isolation. Specific concern: the LiteLLM CVE-2026-42208 (CVSS 9.3, pre-auth SQL injection on the Bearer auth path, exploited within 36 hours of disclosure) is a reminder that any self-hosted DB-backed credential gateway is itself a high-value attack target. Prefer a flat-file or env-only credential store on the sidecar over a database.

  • Helicone is in maintenance mode since the Mintlify acquisition in March 2026 (security fixes only, no features). Treat as legacy.

  • Portkey's virtual-key vault — the actual credential-injection feature — requires the Enterprise plan for self-host. The open-source gateway alone does routing without injection.

Build-vs-adopt synthesis

Architecturally aligned: nono. Phantom-token + explicit-URL routing matches the design recommended here exactly; zero TLS work. But "not security audited" + "early alpha" means adopting it is a bet on the project rather than a buy-vs-build win.

Most mature OSS purpose-built: Infisical Agent Vault. MIT, v0.19.0 active, v0.17.0 added a containerized agent mode that maps directly to bot-bottle. Friction is the TLS-MITM topology — another container-local CA, the Go-loopback workaround, duplication with pipelock's existing TLS interception layer.

For the immediate Anthropic-token slice, a ~100-line Rust or Go reverse proxy modeled on nono's phantom-token shape is probably less work and less risk than adopting either. The surface is small: hold the token, inject one header, forward to api.anthropic.com over TLS, pass through SSE without buffering. For Gitea / GitHub / GitLab the same proxy generalizes by config.

The build path also keeps the credential store flat (env file or mode-600 YAML on the sidecar), which sidesteps the "DB-backed-gateway as attack surface" concern the LiteLLM CVE exposed.

In priority order:

  1. In-container reverse proxy holding CLAUDE_CODE_OAUTH_TOKEN. Highest-leverage change: credential isolation and the ability to drop the api.anthropic.com TLS passthrough in pipelock (see secret-minimization-over-dlp.md §2). Proxy runs as root inside the agent container, listens on 127.0.0.1 (no Go-loopback issue for the reverse-proxy case — the agent isn't using HTTPS_PROXY), injects Authorization: Bearer …, sets the bottle's ANTHROPIC_BASE_URL to the local URL.

  2. Layer in CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 plus the existing pipelock egress allowlist (api.anthropic.com only, plus the per-agent set of MCP / git / package-registry hosts). A hijacked claude can no longer exfil through statsig / Sentry / npm even if it captures something. Also disable Sentry error reporting via DISABLE_ERROR_REPORTING=1.

  3. Generalize the same proxy to forge tokens. Add a manifest field along the lines of forge: { kind: "gitea", url, tokenRef } so a per-bottle token reference resolves at launch, the proxy starts as root before node is exec'd, and tea plus git HTTPS remotes are pre-configured to point at the proxy. Use Authorization: token <…> for Gitea, Bearer for GitHub / GitLab.

  4. Scope-narrow the tokens at issuance. repo:write only, no admin, no user management. Fine-grained GitHub PATs, GitLab project-access tokens, Gitea per-repo tokens. Cheapest single thing to do; bounds blast radius regardless of whether the proxy ships.

  5. Allowlist at the proxy once usage is stable. Method + path filter keyed off the agent's actual API calls; reject everything else. Doesn't prevent abuse within the allowlist but narrows the surface to known good operations.

The current docker run -e CLAUDE_CODE_OAUTH_TOKEN pattern is fine for argv hygiene on the host, but inside the bottle the token is fully exposed. The proxy pattern moves it across a kernel-enforced boundary — the same property the SSH agent already gives us for keys, and the same property the git-gate already gives us for upstream push credentials.

Sources

Mechanics

Landscape