Skip to content
ALTAIRA LABS
DocsBlog

Configure agent authentication

This guide covers securing agent endpoints with the facade’s built-in validator chain. For dashboard authentication, see:

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:

  1. management-plane — admits dashboard-minted JWTs used by the “Try this agent” debug view.
  2. 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.

1. Dashboard access only (no external traffic)

Section titled “1. Dashboard access only (no external traffic)”

Leave spec.externalAuth unset:

apiVersion: omnia.altairalabs.ai/v1alpha1
kind: AgentRuntime
metadata:
name: internal-agent
spec:
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: false

Then 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: sub

claimMapping 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:

Terminal window
kubectl get agentruntime my-agent -o yaml | yq '.status.conditions'
# look for type: OIDCJWKSReady

Provider-specific issuer values:

Provider issuer
Auth0 https://<tenant>.auth0.com/
Okta https://<org>.okta.com/oauth2/default
Google 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.groups

The 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.

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.

const token = await getAccessToken();
const ws = new WebSocket('wss://agents.example.com/my-agent/ws', {
headers: { 'Authorization': `Bearer ${token}` },
});
Terminal window
wscat -H "Authorization: Bearer $TOKEN" \
-c wss://agents.example.com/my-agent/ws
websocat -H "Authorization: Bearer $TOKEN" \
wss://agents.example.com/my-agent/ws

Check the facade logs — rejection telemetry is emitted at V(1):

Terminal window
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 — no spec.externalAuth validator is configured, or the caller presented no credential.
  • reason=invalid credential — the credential format/signature is wrong (expired JWT, unknown client key hash).
Terminal window
kubectl get secret agent-my-agent-oidc-jwks -o yaml
kubectl 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:

  1. Confirm the edge (Istio/gateway) is injecting the expected headers — use a debug container or Envoy access logs.
  2. Confirm the edge strips inbound versions of those headers so clients can’t spoof them.
  3. Confirm spec.externalAuth.edgeTrust.claimsFromHeaders lists the inbound header names exactly as the edge emits them (header names are case-insensitive).

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.