Authenticate users before agents
Pattern: front the CLRK ingress with your own auth layer. CLRK does not authenticate inbound traffic itself.
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
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-Idandtraceparentpropagate 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:
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:
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:
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
- Trace a request from your auth proxy through every span CLRK produces - see Trace requests through agents.
- Make sure the agent your callers reach is bounded - see Cap LLM spend per agent and Lock down agent egress.