Configure agent authentication
This guide covers securing agent endpoints with the facade’s built-in validator chain. For dashboard authentication, see:
- Configure Dashboard Authentication — set up user authentication
- Authentication Architecture — the full auth model
How the facade authenticates a request
Section titled “How the facade authenticates a request”Each agent facade runs an ordered chain of validators. A request is admitted as soon as any validator accepts it; otherwise the facade returns 401.
By default the chain contains:
- management-plane — admits dashboard-minted JWTs used by the “Try this agent” debug view.
- Any data-plane validator configured on the AgentRuntime under
spec.externalAuth(client keys, OIDC, edge-trust).
With no spec.externalAuth configured, the agent is reachable only
from the dashboard — there is no unauthenticated external path. This is
the secure default. Add at least one data-plane validator before
exposing an agent to customer traffic.
Recipes
Section titled “Recipes”1. Dashboard access only (no external traffic)
Section titled “1. Dashboard access only (no external traffic)”Leave spec.externalAuth unset:
apiVersion: omnia.altairalabs.ai/v1alpha1kind: AgentRuntimemetadata: name: internal-agentspec: promptPackRef: { name: internal-pack } providerRefs: - name: openai toolRegistryRef: { name: internal-tools }The dashboard proxy mints a short-lived RS256 token per request and attaches it to the upstream WebSocket. External callers receive 401.
2. Per-caller client keys (managed in the dashboard)
Section titled “2. Per-caller client keys (managed in the dashboard)”Opt the agent in:
spec: externalAuth: clientKeys: defaultRole: premium # any string; surfaces as identity.claims.role trustEndUserHeader: falseThen create keys from the dashboard’s Credentials page — each key is
stored as a Secret keyed by its sha256 hash, with an expiry and an
arbitrary claim map. Clients present Authorization: Bearer <key>; the
facade looks up the hash and admits the caller with the key’s claims
surfaced to ToolPolicy as identity.claims.* (for example
identity.claims.tier == "premium"), un-spoofable because they are bound
to the key at creation rather than sent by the caller.
No CRD edit is required when you add or revoke keys.
For the simplest case of a single shared credential for all callers, create one client key and distribute it — that’s the direct replacement for what used to be a dedicated shared-token mechanism.
3. OIDC (customer IdP — no Istio required)
Section titled “3. OIDC (customer IdP — no Istio required)”Point the facade at your IdP’s issuer. The controller auto-fetches the JWKS from the discovery document and refreshes every 6 hours:
spec: externalAuth: oidc: issuer: "https://auth.example.com" audience: "my-agent" claimMapping: # optional; shown with defaults subject: sub endUser: subclaimMapping only overrides subject/endUser — there’s no role
field. The IdP’s role/group claims (e.g. omnia.role) pass through
unmapped and land in ToolPolicy as identity.claims.<name>, keyed by
whatever claim name the IdP actually uses.
The facade terminates and verifies the JWT in-process — no service mesh
is needed. A per-agent agent-<name>-oidc-jwks Secret appears in the
workspace namespace after the first reconcile; status conditions surface
any discovery or fetch failures:
kubectl get agentruntime my-agent -o yaml | yq '.status.conditions'# look for type: OIDCJWKSReadyProvider-specific issuer values:
| Provider | issuer |
|---|---|
| Auth0 | https://<tenant>.auth0.com/ |
| Okta | https://<org>.okta.com/oauth2/default |
https://accounts.google.com |
|
| Keycloak | https://<host>/realms/<realm> |
| Azure AD | https://login.microsoftonline.com/<tenant-id>/v2.0 |
4. Edge-trust (Istio or API gateway terminates the JWT)
Section titled “4. Edge-trust (Istio or API gateway terminates the JWT)”When an upstream edge (Istio RequestAuthentication with
outputClaimToHeaders, Envoy, or a commercial API gateway) already
terminates the JWT and injects claim headers, trust those headers
instead of re-verifying:
spec: externalAuth: edgeTrust: headerMapping: # defaults match the chart's Istio layout subject: x-user-id endUser: x-user-id email: x-user-email claimsFromHeaders: x-user-groups: groups # exposed to ToolPolicy as identity.claims.groupsThe inbound role header (default x-user-roles — the facade’s built-in
default, see authentication.edgeTrust.defaultStripHeaders in the chart’s
values.yaml) is always read into identity.claims.role — it isn’t
configurable via headerMapping. To surface an edge role header under a
different claim name, or additional role-like headers, list them in
claimsFromHeaders instead.
Combining validators
Section titled “Combining validators”You can configure several at once — they all run:
spec: facades: - type: websocket managementPlane: true # dashboard debug view still works externalAuth: clientKeys: { defaultRole: viewer } oidc: { issuer: "https://auth.example.com", audience: "my-agent" }The facade tries each validator in order and admits the first that
accepts the request. Setting managementPlane: false on a facade entry
blocks the dashboard debug view for that facade — useful for paranoid
workloads that want data-plane-only isolation. managementPlane is set
per facade, so you can admit the management plane on one surface while
isolating another.
Connect with a token
Section titled “Connect with a token”WebSocket (browser or node)
Section titled “WebSocket (browser or node)”const token = await getAccessToken();const ws = new WebSocket('wss://agents.example.com/my-agent/ws', { headers: { 'Authorization': `Bearer ${token}` },});wscat -H "Authorization: Bearer $TOKEN" \ -c wss://agents.example.com/my-agent/ws
websocat -H "Authorization: Bearer $TOKEN" \ wss://agents.example.com/my-agent/wsTroubleshooting
Section titled “Troubleshooting”Every request returns 401
Section titled “Every request returns 401”Check the facade logs — rejection telemetry is emitted at V(1):
kubectl logs -l app.kubernetes.io/name=omnia-agent -c facade --tail=50# look for: "auth middleware rejected request" reason=... path=...Common causes:
reason=no validator admitted— nospec.externalAuthvalidator is configured, or the caller presented no credential.reason=invalid credential— the credential format/signature is wrong (expired JWT, unknown client key hash).
OIDC tokens are rejected
Section titled “OIDC tokens are rejected”kubectl get secret agent-my-agent-oidc-jwks -o yamlkubectl get agentruntime my-agent -o yaml | yq '.status.conditions[] | select(.type=="OIDCJWKSReady")'If the Secret is missing or empty, the controller couldn’t reach the issuer — check reachability from the operator pod and confirm the issuer URL has no trailing slash.
Edge-trust headers aren’t populated downstream
Section titled “Edge-trust headers aren’t populated downstream”Tool handlers see edge-trust claims as X-Omnia-Claim-<name> headers
and ToolPolicy sees them as identity.claims.<name>. If they’re
missing:
- Confirm the edge (Istio/gateway) is injecting the expected headers — use a debug container or Envoy access logs.
- Confirm the edge strips inbound versions of those headers so clients can’t spoof them.
- Confirm
spec.externalAuth.edgeTrust.claimsFromHeaderslists the inbound header names exactly as the edge emits them (header names are case-insensitive).
Migrating from legacy shared-token auth
Section titled “Migrating from legacy shared-token auth”Older releases supported a shared bearer token, both as a per-facade
authentication.secretRef on the A2A HTTP endpoint and later as
spec.externalAuth.sharedToken. Both fields have been removed —
create a single client key
and distribute it as the replacement; every facade in spec.facades
(WebSocket, A2A, MCP, …) validates against spec.externalAuth.clientKeys.