# Authenticate users before agents

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

<Callout label="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.
</Callout>

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

```mermaid
flowchart LR
  I[Internet] --> A["Your auth proxy<br/>(Envoy SecurityPolicy, FastAPI, edge worker)"]
  A --> CLRK[CLRK per-TaskAgent ingress]
  CLRK --> S[Sandbox]
```

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:

```yaml
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:

```python
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:

```yaml
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](./trace-requests-through-agents).
- Make sure the agent your callers reach is bounded - see [Cap LLM
  spend per agent](./cap-llm-spend-per-agent) and [Lock down agent
  egress](./lock-down-agent-egress).
