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

393 lines
23 KiB
Markdown

# 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`](secret-minimization-over-dlp.md)
(the architectural framing — why this matters), and to
[`local-vs-remote-agent-execution.md`](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`](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](https://github.com/anthropics/claude-code/issues/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`](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](https://github.com/go-gitea/gitea/issues/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 | **Yes**`BASE_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.
## Recommended path forward
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`](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
- [Authentication — Claude Code docs](https://code.claude.com/docs/en/authentication)
- [LLM gateway configuration — Claude Code docs](https://code.claude.com/docs/en/llm-gateway)
- [Claude Code environment variables](https://code.claude.com/docs/en/env-vars)
- [GitHub issue anthropics/claude-code#36998 — interactive mode bypasses ANTHROPIC_BASE_URL](https://github.com/anthropics/claude-code/issues/36998)
- [GitHub issue anthropics/claude-code#11587 — apiKeyHelper vs CLAUDE_CODE_OAUTH_TOKEN](https://github.com/anthropics/claude-code/issues/11587)
- [`proc_pid_environ(5)` man page](https://man7.org/linux/man-pages/man5/proc_pid_environ.5.html)
- [Documenting ptrace access mode checking — LWN](https://lwn.net/Articles/692203/)
- [StepSecurity — Claude Code Action outbound network analysis](https://www.stepsecurity.io/blog/anthropics-claude-code-action-security-how-to-secure-claude-code-in-github-actions-with-harden-runner)
- [Manage API key environment variables — Claude Help Center](https://support.claude.com/en/articles/12304248-manage-api-key-environment-variables-in-claude-code)
- [tea source — `cmd/login/add.go`](https://gitea.com/gitea/tea/src/branch/main/cmd/login/add.go)
- [tea source — `modules/config/config.go`](https://gitea.com/gitea/tea/src/branch/main/modules/config/config.go)
- [tea source — `modules/git/auth.go`](https://gitea.com/gitea/tea/src/branch/main/modules/git/auth.go)
- [Gitea API usage docs](https://docs.gitea.com/development/api-usage)
- [go-gitea/gitea#16734 — `Authorization: Bearer` triggers spurious CSRF](https://github.com/go-gitea/gitea/issues/16734)
- [golang/go#28866 — `net/http` ignores `HTTPS_PROXY` for `127.0.0.1`/`localhost`](https://github.com/golang/go/issues/28866)
- [git credential helper docs](https://git-scm.com/docs/gitcredentials)
### Landscape
- [Docker AI Sandboxes — credentials](https://docs.docker.com/ai/sandboxes/security/credentials/)
- [docker/desktop-feedback#130 — custom injection rules](https://github.com/docker/desktop-feedback/issues/130)
- [Cloudflare Sandbox Auth blog](https://blog.cloudflare.com/sandbox-auth/)
- [Cloudflare Outbound Workers GA changelog](https://developers.cloudflare.com/changelog/post/2026-04-13-sandbox-outbound-workers-tls-auth/)
- [Cloudflare Sandboxes GA — InfoQ](https://www.infoq.com/news/2026/04/cloudflare-sandboxes-ga/)
- [Infisical agent-vault — GitHub](https://github.com/Infisical/agent-vault)
- [Infisical agent-vault — releases](https://github.com/Infisical/agent-vault/releases)
- [Infisical agent-vault — blog](https://infisical.com/blog/agent-vault-the-open-source-credential-proxy-and-vault-for-agents)
- [nono — GitHub](https://github.com/always-further/nono)
- [nono — phantom token blog](https://nono.sh/blog/blog-credential-injection)
- [Aegis — GitHub](https://github.com/getaegis/aegis)
- [OneCLI — GitHub](https://github.com/onecli/onecli)
- [Sandbox0 — GitHub](https://github.com/sandbox0-ai/sandbox0)
- [Buildkite Cleanroom — GitHub](https://github.com/buildkite/cleanroom)
- [Aembit IAM for Agentic AI — GA](https://aembit.io/blog/aembit-iam-for-agentic-ai-is-now-generally-available/)
- [Aembit Claude integration docs](https://docs.aembit.io/user-guide/access-policies/server-workloads/guides/claude)
- [LiteLLM CVE-2026-42208 — Sysdig writeup](https://www.sysdig.com/blog/cve-2026-42208-targeted-sql-injection-against-litellms-authentication-path-discovered-36-hours-following-vulnerability-disclosure/)
- [LiteLLM — GitHub](https://github.com/BerriAI/litellm)
- [Portkey + Claude Code](https://portkey.ai/docs/virtual_key_old/integrations/libraries/claude-code)
- [Portkey gateway — GitHub](https://github.com/Portkey-ai/gateway)
- [Helicone maintenance mode announcement](https://dev.to/torrixai/helicone-is-now-in-maintenance-mode-here-is-how-to-switch-to-a-self-hosted-alternative-in-5-4li0)
- [LangSmith LLM auth proxy docs](https://docs.langchain.com/langsmith/llm-auth-proxy-self-hosted)
- [AWS IMDSv2 docs](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html)
- [Pipelock — Help Net Security](https://www.helpnetsecurity.com/2026/05/04/pipelock-open-source-ai-agent-firewall/)
- [CB4A IETF draft — Credential Broker for Agents](https://www.ietf.org/archive/id/draft-hartman-credential-broker-4-agents-00.html)
- [List of coding agent sandboxes (May 2026)](https://gist.github.com/wincent/2752d8d97727577050c043e4ff9e386e)