Hide credentials from agents
Keep API keys out of the agent process entirely. The proxy injects them after the request leaves the sandbox.
The cheapest threat-model improvement you can make to an agent system is to remove the credential from the agent process. Prompt injection, model jailbreak, tool-call abuse - every one of them ends at a process that doesn't hold the key. This guide shows how CLRK delivers that with four standard objects.
The threat model in one paragraph
An agent that holds an API key is one prompt away from leaking it.
The defense is to never put the key in the agent's process
environment. The egress proxy holds it, terminates the agent's
outbound TLS connection, swaps a placeholder header for the real one,
and re-emits the request to the upstream over a fresh connection.
The agent sees only clrk-injected-by-proxy.
The four objects
Secret- standard Kubernetes Secret. Holds the real key.EgressGateway- the TLS-terminating MITM hop every outbound connection from the sandbox traverses. Must declare aTerminatelistener (the example setstls.mode: Terminate) so the proxy can read and rewrite request headers.AIProviderRoute- declares the upstream and the provider parser. Accepted provider values today (a registry-validated string, not a kubebuilder enum):openai,anthropic,google,azure-openai,bedrock,custom.CredentialInjectionPolicy- attaches the Secret to a route and declares where the credential goes (Header,QueryParam, or provider-specificProviderAuthlike AWSv4 / GCPServiceAccount).
Worked example: Anthropic
Pulled from _examples/echo-bot/manifests/. The four objects:
# 1. The Secret. clrk dev --secret materializes it from your shell env;
# in prod the operator creates it normally.
apiVersion: v1
kind: Secret
metadata:
name: anthropic-credentials
data:
api-key: <base64-of-sk-ant-...>
---
# 2. The EgressGateway with a TLS-Terminate listener. allow-all here
# because the example only egresses to Anthropic; production
# deployments typically pair this with deny-all + an L4 allowlist.
apiVersion: clrk.apoxy.dev/v1alpha1
kind: EgressGateway
metadata:
name: echo-bot
spec:
defaultPolicy: allow-all
listeners:
- name: egress
protocol: TLS
tls:
mode: Terminate
otlp:
captureBody:
maxBytes: 65536
---
# 3. The AIProviderRoute. Declares Anthropic and the endpoints to
# intercept.
apiVersion: clrk.apoxy.dev/v1alpha1
kind: AIProviderRoute
metadata:
name: anthropic
spec:
parentRefs:
- group: clrk.apoxy.dev
kind: EgressGateway
name: echo-bot
rules:
- matches:
- provider: anthropic
endpoints:
- /v1/messages
- /v1/complete
---
# 4. The CredentialInjectionPolicy. Binds the Secret to the route and
# declares the header to overwrite.
apiVersion: clrk.apoxy.dev/v1alpha1
kind: CredentialInjectionPolicy
metadata:
name: anthropic
spec:
parentRefs:
- group: clrk.apoxy.dev
kind: AIProviderRoute
name: anthropic
secretRef:
name: anthropic-credentials
secretKey: api-key
target: Header
headerName: x-api-keyThe agent's HTTPS request to api.anthropic.com/v1/messages arrives
at the EgressGateway with whatever x-api-key value the agent
supplied (typically the placeholder clrk-injected-by-proxy). The
proxy rewrites the header from the Secret and forwards.
Where in the request lifecycle the swap happens
The sandbox trusts the per-EgressGateway CA because the worker installed its leaf cert as a trust anchor at sandbox boot. The agent's HTTPS library accepts the EG's MITM cert as if Anthropic itself had presented it. From the agent's perspective the connection looks normal.
Confirm via OTLP, not via /proc
The authority is the captured request, not the agent's process
environment. Two things to verify in the otel-traces pane:
- The call returned 200. Anthropic 401s on a wrong key. A 200
with non-zero
gen_ai.usage.*_tokensproves the proxy supplied a valid credential. - The captured request shows the credential header redacted.
Expand the request span's
http.request.headersevent and confirmhttp.request.header.x-api-keyreads[redacted]- CLRK replaces known credential headers (authorization,x-api-key,anthropic-api-key, etc.) with[redacted]before exporting telemetry, so the real value never ships either. Header values are span event attributes attached unconditionally, not part of the body. Theotlp.captureBody.maxBytes: 65536setting only controls the request/response body bytes (captured base64 in thehttp.request.bodyevent), not headers.
Smuggle defense
If the agent script sets its own x-api-key header in an attempt to
override the policy, the policy still wins - the proxy rewrites the
header unconditionally based on the route + policy attachment.
There's no agent-controllable opt-out. The swap happens on the egress
ext_proc path (the egress-extproc component), so the captured
request in the otel-traces / otel-logs panes reflects the
injected credential, not the agent-supplied one.
Key rotation
Rotate the Secret in place. The proxy reads the latest value via the controller's informer cache, so changes propagate without restarting the agent or the gateway:
clrk secret set anthropic-credentials --from-literal=api-key="$NEW_KEY"The next request through the gateway picks up the new value.
BYOK and per-customer keys
The simplest pattern is one AIProviderRoute + one
CredentialInjectionPolicy per customer, each bound to its own
Secret. Route customer requests through the right TaskAgent (so the
right EgressGateway, so the right policy). This works today with
one-policy-per-route attachment.
Per-request credential selection - a single agent picking which customer's key to use based on a header on the inbound request - is not supported as a first-class feature today. Talk to us if you need it.
What this does NOT do
- Does not authenticate the agent's caller. Inbound auth is your problem; see Authenticate users before agents.
- Does not encrypt traffic between the agent and the proxy end-to-end. The proxy decrypts in order to rewrite. If your compliance regime requires no plaintext at any hop, this approach is not for you.
- Does not protect against an agent leaking the response. Once Anthropic returns content to the agent, the agent owns it. Pair with output policy and audit if that matters.
- Does not work with secrets sourced from
spec.template.spec.env[].valueFrom.secretKeyRef. That field is silently dropped today. Credential injection is the intended path; do not try to surface secrets to the agent process.
Where to next
- Pair credential injection with a default-deny egress allowlist so a stolen-key request can only reach allowed upstreams - see Lock down agent egress.
- Cap how many tokens a single agent (or a misbehaving prompt) can burn against the injected key - see Cap LLM spend per agent.
- Trace which request triggered which credentialed call - see Trace requests through agents.