Keep your API keys out of untrusted code. paude-proxy is a credential-injecting HTTPS proxy — it holds your real API keys and injects them into outgoing requests based on the destination domain. Your application sends requests with dummy credentials; the proxy swaps in the real ones before they hit the wire. The application never sees, stores, or can exfiltrate valid credentials.
Your app paude-proxy api.openai.com
(no real credentials) (has real credentials)
| |
|-- Authorization: dummy -->|-- Authorization: Bearer sk-REAL-KEY
| |-- forwards to upstream
|<-- response --------------|<-- upstream response
This is especially useful for AI coding agents (Claude Code, Cursor, Gemini CLI) running in sandboxed containers, but works for any scenario where you want to broker API credentials without exposing them to the calling code.
Requires Go 1.23+.
# Build
make build
# Start the proxy with your real OpenAI key
OPENAI_API_KEY=sk-your-real-key \
ALLOWED_DOMAINS=.openai.com \
PAUDE_PROXY_CA_DIR=/tmp/proxy-ca \
./bin/paude-proxyIn another terminal:
# Make a request through the proxy — your real key is injected automatically
curl --proxy-cacert /tmp/proxy-ca/ca.crt \
-x http://localhost:3128 \
https://api.openai.com/v1/models
# The request arrives at OpenAI with "Authorization: Bearer sk-your-real-key"
# even though curl never knew the real keyThat's it. Any HTTPS client that speaks HTTP CONNECT can use the proxy.
paude-proxy is a man-in-the-middle (MITM) HTTPS proxy built on goproxy. When a client connects:
- Client sends
CONNECT api.openai.com:443 - Proxy terminates TLS using a generated CA certificate (the client must trust this CA)
- Client sends the HTTP request (with dummy or no credentials)
- Proxy looks up the destination domain in its credential routing table
- If a match is found, the proxy replaces the auth header with real credentials
- Proxy forwards the request over a new TLS connection to the real upstream
- Response is passed back to the client unmodified
The proxy always overrides auth headers — it never trusts what the client sends. This is by design: the client should have dummy placeholder credentials that satisfy SDK initialization, and the proxy replaces them with real values.
ANTHROPIC_API_KEY=sk-ant-real-key \
OPENAI_API_KEY=sk-real-key \
GH_TOKEN=ghp_real-token \
ALLOWED_DOMAINS=.anthropic.com,.openai.com,github.com,.githubusercontent.com \
PAUDE_PROXY_CA_DIR=/tmp/proxy-ca \
./bin/paude-proxyRequests to *.anthropic.com get x-api-key: sk-ant-real-key. Requests to *.openai.com get Authorization: Bearer sk-real-key. Requests to github.com get Authorization: Bearer ghp_real-token. Requests to any other domain are blocked by the domain filter.
Set the standard proxy environment variables so your application routes traffic through paude-proxy:
export HTTP_PROXY=http://localhost:3128
export HTTPS_PROXY=http://localhost:3128
# Your app can use dummy credentials — the proxy injects the real ones
export ANTHROPIC_API_KEY=paude-proxy-managed
export OPENAI_API_KEY=paude-proxy-managed
# Run your application
python my_agent.pyThe application initializes SDKs with the dummy keys (satisfying any client-side validation), sends requests through the proxy, and the proxy injects real credentials.
Since the proxy terminates TLS, clients must trust the generated CA. The CA cert is written to $PAUDE_PROXY_CA_DIR/ca.crt at startup.
System-wide (RHEL/CentOS/Fedora):
cp /tmp/proxy-ca/ca.crt /etc/pki/ca-trust/source/anchors/paude-proxy.crt
update-ca-trustSystem-wide (Ubuntu/Debian):
cp /tmp/proxy-ca/ca.crt /usr/local/share/ca-certificates/paude-proxy.crt
update-ca-certificatesPer-runtime (no root required):
# Node.js
export NODE_EXTRA_CA_CERTS=/tmp/proxy-ca/ca.crt
# Python (requests, pip, httpx)
export REQUESTS_CA_BUNDLE=/tmp/proxy-ca/ca.crt
export SSL_CERT_FILE=/tmp/proxy-ca/ca.crt
# Go
export SSL_CERT_FILE=/tmp/proxy-ca/ca.crt
# curl
curl --proxy-cacert /tmp/proxy-ca/ca.crt -x http://localhost:3128 https://example.comRun the proxy alongside an application container, sharing the CA cert via a volume:
# Create a shared volume for the CA cert
podman volume create proxy-ca
# Start the proxy with real credentials
podman run -d --name proxy \
-e ANTHROPIC_API_KEY=sk-ant-real-key \
-e OPENAI_API_KEY=sk-real-key \
-e ALLOWED_DOMAINS=.anthropic.com,.openai.com \
-e PAUDE_PROXY_CA_DIR=/data/ca \
-v proxy-ca:/data/ca \
-p 3128:3128 \
paude-proxy:latest
# Start your application container — no real credentials needed
podman run -d --name my-app \
-e HTTP_PROXY=http://proxy:3128 \
-e HTTPS_PROXY=http://proxy:3128 \
-e ANTHROPIC_API_KEY=paude-proxy-managed \
-e NODE_EXTRA_CA_CERTS=/ca/ca.crt \
-v proxy-ca:/ca:ro \
my-app-imageThe application container has no real API keys — only dummy placeholders. The proxy container holds the real credentials and injects them.
Use PAUDE_PROXY_ALLOWED_CLIENTS to restrict which IPs can connect to the proxy:
# Only allow connections from 10.0.0.5
PAUDE_PROXY_ALLOWED_CLIENTS=10.0.0.5 \
OPENAI_API_KEY=sk-real-key \
PAUDE_PROXY_CA_DIR=/tmp/proxy-ca \
./bin/paude-proxyThis prevents other containers or processes on the network from using the proxy to get credential injection. For container environments, network isolation (dedicated networks or Kubernetes NetworkPolicy) is the primary access control; source IP filtering is an additional layer.
| Env Var | Domain Pattern | Header Injected |
|---|---|---|
ANTHROPIC_API_KEY |
*.anthropic.com |
x-api-key: <key> |
OPENAI_API_KEY |
*.openai.com |
Authorization: Bearer <key> |
CURSOR_API_KEY |
*.cursor.com, *.cursorapi.com |
Authorization: Bearer <key> |
GH_TOKEN |
github.com, api.github.com, *.githubusercontent.com |
Authorization: Bearer <pat> |
GOOGLE_APPLICATION_CREDENTIALS |
*.googleapis.com |
Authorization: Bearer <token> (auto-refreshed OAuth2) |
Only set the env vars for the providers you need. If an env var is unset, the proxy passes requests to that domain through without credential injection.
All configuration is via environment variables:
| Variable | Description | Default |
|---|---|---|
PAUDE_PROXY_LISTEN |
Listen address | :3128 |
PAUDE_PROXY_CA_DIR |
Directory for CA cert/key (persists across restarts if mounted) | /data/ca |
PAUDE_PROXY_VERBOSE |
Enable verbose logging (1/0) |
0 |
PAUDE_PROXY_ALLOWED_CLIENTS |
Comma-separated IPs/CIDRs allowed to connect | (all) |
ALLOWED_DOMAINS |
Comma-separated domain allowlist (empty = allow all) | |
ALLOWED_OTEL_PORTS |
Comma-separated extra allowed ports | |
BLOCKED_LOG_PATH |
Path for blocked-request log file | /tmp/squid-blocked.log |
PAUDE_PROXY_CREDENTIALS_CONFIG |
Path to custom credential routing config (JSON) | (embedded default) |
ALLOWED_DOMAINS supports three formats:
- Exact:
api.example.com— matches onlyapi.example.com - Wildcard suffix:
.example.com— matchesexample.comand all subdomains - Regex:
~pattern— matches hostnames against the regex
Example: github.com,.openai.com,~aiplatform\.googleapis\.com$
The default credential routing table (shown above) is embedded in the binary. To customize which credentials are injected for which domains, provide a JSON config file via PAUDE_PROXY_CREDENTIALS_CONFIG:
PAUDE_PROXY_CREDENTIALS_CONFIG=/path/to/credentials.json ./bin/paude-proxyThe config file maps environment variables to injector types and domain patterns:
{
"credentials": [
{
"env_var": "ANTHROPIC_API_KEY",
"injector": "api_key",
"params": { "header_name": "x-api-key" },
"domains": [".anthropic.com"]
},
{
"env_var": "OPENAI_API_KEY",
"injector": "bearer",
"domains": [".openai.com"]
},
{
"env_var": "GH_TOKEN",
"injector": "bearer",
"domains": ["github.com", "api.github.com", ".githubusercontent.com"]
},
{
"env_var": "GOOGLE_APPLICATION_CREDENTIALS",
"injector": "gcloud",
"domains": [".googleapis.com"]
}
]
}Available injector types:
| Type | Description | Required params |
|---|---|---|
bearer |
Sets Authorization: Bearer <value> |
— |
api_key |
Sets a custom header with the credential value | header_name |
gcloud |
OAuth2 Bearer token from ADC (auto-refreshed); also enables token vending | — |
Domain patterns: Prefix with . for wildcard suffix matching (.openai.com matches api.openai.com). Without a prefix, matches exactly (github.com matches only github.com).
Credential values always come from environment variables — never put secrets in the config file. If an env var is unset, its entry is silently skipped.
The default config is at internal/credentials/credentials.json.
For Google Cloud APIs, the proxy uses a two-step approach:
- Token vending: The client has a stub ADC file with a dummy
refresh_token. When its Google Auth library POSTs tooauth2.googleapis.com/token, the proxy intercepts and returns a dummy access token. - Credential injection: When the client calls
*.googleapis.comwith the dummy Bearer token, the proxy replaces it with a real OAuth2 token from its own ADC.
The client never sees any real credential — not the refresh token, not even a short-lived access token.
make build # Build binary to bin/paude-proxy
make test # Run all tests
make lint # go vet
make docker # Build container image with podman
make run # Build and run locallyRequires Go 1.23+. After cloning, run go mod tidy to resolve dependencies.
The proxy is designed for scenarios where the client is untrusted (e.g., an AI agent that could be prompt-injected into attempting credential exfiltration). It protects against:
- Credential theft from filesystem/env — real credentials only exist in the proxy process, never in the client's environment
- Credentials sent to wrong domains — routing table with strict suffix matching (
evil-openai.comdoes NOT match.openai.com) - Host header forgery — credential routing uses the CONNECT target (from the TCP connection), not the Host header
- Redirect-based credential leakage — proxy never follows redirects; 3xx responses pass through to the client
- Unauthorized proxy access — source IP filtering + network isolation
Not in scope: preventing the client from misusing credentials for their intended service (e.g., using a GitHub PAT to push code). Mitigate with fine-grained, least-privilege tokens.
This proxy was built for the paude project, which orchestrates AI coding agents in containers. paude handles:
- Running the proxy as a separate container alongside agent containers
- Copying the CA cert to agent containers and configuring trust
- Setting dummy placeholder credentials in agent containers
- Writing stub ADC files for gcloud auth flow
See docs/PAUDE_INTEGRATION.md for integration details.
- Aegis — TypeScript, static API keys only, no gcloud ADC, no MITM
- Envoy + Go filter — MITM forward proxy with dynamic cert generation is complex in Envoy; goproxy is purpose-built for this
- Squid with ssl-bump — Would need ICAP/eCAP adapter for credential injection; much more complex for the same result
MIT — see LICENSE