Getting startedGuidesReferenceChangelog
Apoxy:// Docs / Guides / Authenticate users before agents

Authenticate users before agents

Pattern: front the CLRK ingress with your own auth layer. CLRK does not authenticate inbound traffic itself.

Pattern guide

This is a pattern guide, not a runnable tutorial. The configurations below are sketches you adapt to your stack. The contract CLRK expects is small and documented; everything else is your call.

CLRK does not authenticate the requests that reach its ingress. There is no built-in OIDC validator, no JWT verifier, no API-key middleware. That is by design - different teams want different things (OAuth, OIDC, JWT, HMAC, mTLS, signed-JWS-on-customer-requests). This guide describes the shape of the auth tier you put in front, the contract that tier must preserve, and the places it integrates with CLRK's identity model.

The deployment shape

$diagramMERMAID

Your auth proxy is the public-facing edge. CLRK's ingress is a private hop the proxy forwards to after validating the caller. The proxy can live anywhere - at the cluster edge as Envoy with SecurityPolicy.jwt, as a sidecar Cloud Run service, as a Cloudflare Worker, as a small FastAPI app on the cluster.

The contract your proxy must preserve

For CLRK's ingress to dispatch the request correctly, the proxy must:

  • Set X-Clrk-TaskAgent: <namespace>/<name>. This is the only header the ingress treats as required. Missing it gets a 400.
  • Preserve the request body byte-for-byte. The dispatcher wraps the body in a CloudEvents envelope and hands it to the agent. Don't compress, don't transcode, don't re-encode.
  • Keep timeout headroom. CLRK's ingress pins the HTTPRoute timeout to the TaskAgent.spec.timeout (default 100s). Your proxy's idle timeout must be at least as long, or the response truncates from the proxy side.

Optional but useful:

  • Forward correlation headers. X-Request-Id and traceparent propagate into the trace chain. Set them at the auth tier so every span in CLRK's OTLP output is joinable back to your edge logs.
  • Strip credentials. Once you've authed the caller, remove Authorization, raw cookies, and any provider key headers from the request you forward. CLRK doesn't need them, and the more surface area you leave on the request, the more an injected prompt can leak by accident.

Sketch: Envoy Gateway SecurityPolicy (OIDC)

The cleanest case if you're already running Envoy Gateway in front of CLRK. Attach a SecurityPolicy to the per-TaskAgent Gateway the ingress materializes:

$terminalYAML
apiVersion: gateway.envoyproxy.io/v1alpha1 kind: SecurityPolicy metadata: name: jq-bot-auth spec: targetRefs: - group: gateway.networking.k8s.io kind: Gateway name: jq-bot oidc: provider: issuer: https://your-issuer.example.com clientID: <your-client-id> clientSecretRef: name: oidc-client-secret redirectURL: https://api.example.com/oauth/callback scopes: [openid, profile, email]

After OIDC validation Envoy attaches the verified identity claims as headers. Forward the ones you want CLRK to see - typically a tenant identifier and a user identifier - and strip the rest.

Sketch: a tiny FastAPI middleware

For teams running Python infra in front of CLRK:

$terminalPY
from fastapi import FastAPI, Request, HTTPException import httpx, jwt app = FastAPI() CLRK_INGRESS = "http://clrk-jq-bot.clrk.svc.cluster.local" @app.post("/{path:path}") async def proxy(path: str, request: Request): token = request.headers.get("authorization", "").removeprefix("Bearer ") try: claims = jwt.decode(token, KEY, algorithms=["RS256"], audience=AUD) except jwt.PyJWTError as e: raise HTTPException(401, str(e)) body = await request.body() async with httpx.AsyncClient(timeout=120.0) as client: r = await client.post( f"{CLRK_INGRESS}/{path}", content=body, headers={ "content-type": request.headers.get("content-type", ""), "X-Clrk-TaskAgent": "default/jq-bot", "X-Tenant-Id": claims["tenant"], "X-User-Id": claims["sub"], "traceparent": request.headers.get("traceparent", ""), }, ) return Response(content=r.content, status_code=r.status_code, headers={"content-type": r.headers.get("content-type", "")})

Forward X-Tenant-Id / X-User-Id into the CloudEvents envelope so the agent can read them. Pair with the identity-extractor wiring below if you want CLRK itself to know about identity for attribution.

Sketch: a Cloudflare Worker

For a fully edge-resident auth tier, a CF Worker can validate JWTs and forward to a CLRK ingress exposed via Tailscale or a private load balancer. The same contract applies: set X-Clrk-TaskAgent, preserve body, set traceparent, strip raw credentials. The Worker runtime keeps you off your own infra entirely for the auth hop.

CLRK's identity model: not authentication

The TaskAgent.spec.identity.extractors field looks like authentication and is not. It is identity extraction - a way to lift a caller identifier from the request and stamp it into OTLP and controller-manager logs. There is no rejection step:

$terminalYAML
spec: identity: extractors: - type: Header header: name: X-User-Id field: user.id - type: JWT jwt: # claim extraction - does NOT verify the JWT signature ...

This is useful after your auth tier has done its job. Have the proxy set X-User-Id to a verified subject, then the extractor makes that identity visible to CLRK's attribution surface. The verification still happens upstream.

What CLRK gives you in return

Once authenticated, your proxy gets a fully observable chain back. Every egress call the agent makes inherits the traceparent your proxy injected. Every OTLP span carries the agent.* and invocation.id attributes. You correlate edge logs to OTLP via traceparent; you correlate OTLP to your data tier via tenant identifiers your agent reads from the envelope.

What's still missing

  • No first-party OIDC/JWT in CLRK ingress. Coming soon. Use an external proxy until then.
  • No per-request credential selection on the egress side based on inbound identity. The proxy can't (yet) say "this caller's tenant maps to this OpenAI key" without a custom Envoy filter. Talk to us if you need this.

Where to next