Getting startedGuidesReferenceChangelog
Apoxy:// Docs / Guides / Hide credentials from agents

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

$diagramMERMAID
  • Secret - standard Kubernetes Secret. Holds the real key.
  • EgressGateway - the TLS-terminating MITM hop every outbound connection from the sandbox traverses. Must declare a Terminate listener (the example sets tls.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-specific ProviderAuth like AWSv4 / GCPServiceAccount).

Worked example: Anthropic

Pulled from _examples/echo-bot/manifests/. The four objects:

$terminalYAML
# 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-key

The 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

$diagramMERMAID

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:

  1. The call returned 200. Anthropic 401s on a wrong key. A 200 with non-zero gen_ai.usage.*_tokens proves the proxy supplied a valid credential.
  2. The captured request shows the credential header redacted. Expand the request span's http.request.headers event and confirm http.request.header.x-api-key reads [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. The otlp.captureBody.maxBytes: 65536 setting only controls the request/response body bytes (captured base64 in the http.request.body event), 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:

$terminalSH
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