Investigates whether the Gitea `tea` CLI can be authenticated via a header-injecting proxy so the token never enters the container — even as an env var. Parallels the OAuth-token research note. Recommends an in-container root-owned reverse proxy as the lowest-friction shape, and flags the unavoidable tradeoff that the agent retains the token's full API scope (no exfil ≠ no harm). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
10 KiB
Isolating the Gitea tea token via an auth-injecting proxy
Research into whether authentication for the tea CLI (Gitea's command-line
client) can be brokered by a proxy so the access token never enters the
container — not as an env var, not in ~/.config/tea/config.yml. Parallel
question to oauth-token-exposure-to-claude.md, but for the Gitea credential
rather than the Anthropic one.
Summary
Yes. tea itself has no credential-helper hook, so the leverage point is on
the wire: a root-owned reverse proxy inside the container holds the token and
injects Authorization: token <…> on every forwarded request. tea is
configured with --url pointing at the proxy and a dummy/empty token; the
proxy talks to the real Gitea over TLS. The same kernel boundary that hides
the OAuth token in the proxy pattern (a node-uid claude cannot read
/proc/<root-pid>/environ under default Docker) hides the Gitea token here.
Git HTTPS push gets the same treatment by rewriting the remote URL to go
through the proxy. The unavoidable tradeoff is that a hijacked claude can
use the token's full scope but cannot exfiltrate the bytes — a strict
improvement over the env-var status quo, not a panacea.
How tea authenticates
tea reads the token from three places, in precedence order:
GITEA_SERVER_TOKENenv var —cmd/login/add.goregisters it viacli.EnvVars("GITEA_SERVER_TOKEN").~/.config/tea/config.yml(XDG) or~/.tea/tea.yml(legacy fallback) — the token is stored in plaintext YAML under thetokenfield of the login entry.- OS credstore — only for OAuth logins; 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. The only ways to feed tea
a token are env var or config file, both of which are readable by the
process holding them. 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. This matters: a credential-helper
shim alone won't intercept tea repo clone — the proxy has to sit on the
HTTP path itself.
Why a proxy is the only credible boundary
Same logic as the OAuth note. Linux has no per-env-var ACL: once a var is in
a process's environ, the process owns it. The lever is process boundary,
not env-var ACL. Default Docker enforces that boundary at the kernel level
via ptrace_may_access: a node-uid claude trying to read a root-owned
proxy's /proc/<pid>/environ gets EACCES without CAP_SYS_PTRACE,
CAP_PERFMON, or --privileged. claude-bottle uses none of those.
Gitea's API is friendly to header-injecting proxies: token auth is
stateless, no CSRF, no request signing, no per-request nonces. Use the
Authorization: token <…> form; an old Gitea bug
(go-gitea/gitea#16734)
emitted spurious "missing CSRF token" errors for the Bearer form on some
endpoints. The fix landed upstream, but token has always been the
header-safe choice.
Proxy architectures
Four shapes worth comparing:
-
In-container reverse proxy (recommended). Root-owned process inside the container listens on a non-loopback address (e.g. a Docker bridge IP or an alias). Token is passed via
docker run -e GITEA_TOKEN, inherited only by the root proxy, never bynode.tea login add --url http://<proxy>:<port>writes a config file whosetokenfield is empty or a dummy. Git HTTPS uses a rewritten remote pointing at the same proxy. Pros: simple, self-contained per agent, no host changes, no MITM CA. Cons: requires a non-loopback bind (see below). -
In-container forward proxy with TLS termination. Root-owned mitmproxy intercepts outbound HTTPS, terminates with a container-local CA, injects the header, re-encrypts.
teakeeps the real Gitea URL andHTTPS_PROXYpoints at the proxy. Critical Go quirk:net/httpignoresHTTPS_PROXYwhen the proxy address is127.0.0.1orlocalhost(golang/go#28866). Workaround is the same — bind on a non-loopback address — and you also pay for CA trust setup. Worth it only if you need transparent interception of multiple unrelated hosts. -
Host-side proxy. Proxy runs on macOS; container reaches it via
host.docker.internal:<port>. The UDS-across-VM constraint already noted in CLAUDE.md (Docker Desktop on macOS does not forward unix-socketconnect()across the VM) does not apply — TCP viahost.docker.internalworks fine, and the Go loopback bypass isn't an issue because the target is not127.0.0.1. Pros: token stays entirely outside the Linux VM. Cons: a host daemon to maintain, and the published port is reachable by any container on the host unless firewalled. This is the architecture Docker's own AI Sandbox product uses. -
Sidecar container. Token-holding container in a shared Docker network. Pros: clean isolation, portable across hosts. Cons: a second container to orchestrate per agent; the token is in another container's env, which is a lateral move rather than a deeper boundary unless the sidecar runs with stricter isolation than the agent container.
For claude-bottle's threat model — local Docker, per-agent containers, already comfortable with root-owned helpers (the SSH agent precedent) — the in-container reverse proxy is the lowest-friction option that gives the desired property.
Caveats and gotchas
- Bind on a non-loopback address. Required for forward-proxy use because
of golang/go#28866; harmless for the reverse-proxy case but worth doing
consistently so the same proxy works for both shapes. A Docker network
alias or
ip addr add 10.0.0.1/32 dev loworks. - Use
Authorization: token <…>, notBearer. Avoids the legacy CSRF-error path on older Gitea versions. teagit operations bypass git's credential.helper. A credential helper shim is not enough; the proxy must sit on the HTTP path. Plaingit pushfrom 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.- Token scope is the blast radius. A pass-through proxy grants the
agent the token's full API scope. Mitigate with fine-grained Gitea token
scopes (
repo:writeonly, noadmin), an HTTP method/path allowlist at the proxy, rate limits, and audit logging. None of these prevent abuse — they bound and observe it. - No exfil isn't no harm. A hijacked claude can still push branches,
open PRs, and do whatever the token's scope permits. Pair the proxy with
the egress-guard work in
network-egress-guard.mdfor the full benefit; the two compose cleanly because the proxy is itself an explicit egress endpoint. teaconfig file is no longer authoritative. The launcher must runtea login addagainst the proxy URL (or write a config file directly) before claude starts, otherwise the agent will hit "no logins configured." Empty-token configs are accepted.
Prior art
This is a known pattern with several recent named implementations:
- Docker AI Sandboxes — host-side intercepting proxy that overwrites the
auth header; token stays on host, container sees a
proxy-managedplaceholder. Closest analog to what claude-bottle would build. - Cloudflare Sandbox Auth — programmable egress with per-sandbox MITM CA for credential injection.
- Infisical agent-vault — open-source TLS-intercepting forward proxy purpose-built for AI-agent workloads. Research preview as of early 2026.
- AWS IMDSv2 — the canonical credential broker on
169.254.169.254; same shape, different problem domain.
Recommended path forward
In priority order:
- In-container reverse proxy holding the Gitea token. Add a manifest
field (e.g.
gitea: { url, tokenRef }) so a per-agent token reference resolves at launch time, the proxy starts as root beforenodeis exec'd, andteaplus git remotes are pre-configured to point at the proxy. Reuse the same root-owned-helper pattern the SSH agent already establishes. - Scope-narrow the Gitea token at issuance —
repo:writefor the target repo, noadmin, no user management. This is the cheapest single thing to do and bounds blast radius regardless of whether the proxy ships. - Allowlist at the proxy once usage is stable. Method + path filter keyed off the agent's actual Gitea calls; reject everything else.
- Compose with
network-egress-guard.md. The proxy is one egress endpoint; the egress guard enforces that nothing else escapes.
The current docker run -e GITEA_TOKEN-style pattern is fine for argv
hygiene, but inside the container the token is fully exposed to claude.
The proxy moves it across a kernel-enforced boundary — same property the
SSH agent already gives us for keys.
Sources
- tea source —
cmd/login/add.go - tea source —
modules/config/config.go - tea source —
modules/git/auth.go - Gitea API Usage docs
- go-gitea/gitea#16734 —
Authorization: Bearertriggers spurious CSRF error - golang/go#28866 —
net/httpignoresHTTPS_PROXYfor127.0.0.1/localhost - Docker AI Sandbox credentials docs
- Cloudflare Sandbox Auth blog
- Infisical agent-vault — GitHub
- Infisical agent-vault — blog post
- AWS IMDSv2 documentation
- git credential helper docs