Skip to content

Configure agent authentication

Add one or more validators under spec.externalAuth so that your applications can call an agent. For how the validator chain works, see Authentication architecture. For every field, see AgentRuntime: externalAuth. For dashboard sign-in, see Configure dashboard authentication.

With no spec.externalAuth, the external listener returns 401 to every request. Only the dashboard’s “Try this agent” view reaches the agent, over the internal management twin.

  • An AgentRuntime with at least one facade, for example my-agent in namespace my-namespace.
  • For client keys: a checkout of the Omnia repository (for hack/mint-client-key.sh), kubectl and openssl.
  • For OIDC: an identity provider whose discovery document the operator can reach.
  • For edge-trust: an edge that verifies the JWT and injects claim headers.

Use clientKeys for a few known applications, oidc for end users who hold tokens from your identity provider, and edgeTrust when a gateway or mesh already verifies the JWT. You can enable all three. The facade tries them in that order and admits the first that accepts the request. A validator that recognises a credential and rejects it, such as an expired JWT, ends the chain.

  1. Enable the validator. An empty object is enough:

    spec:
    externalAuth:
    clientKeys: {}

    Apply it with kubectl apply -f. Add defaultRole or trustEndUserHeader under clientKeys if you need them.

  2. Mint a key:

    Terminal window
    hack/mint-client-key.sh \
    --context my-cluster \
    --namespace my-namespace \
    --agent my-agent \
    --id terraform \
    --role premium \
    --expires-at 2027-01-01T00:00:00Z

    The script requires --context. Add --claim key=value for further claims. It prints the raw token to stdout once. The cluster stores only the SHA-256 hash, so capture the token or mint another key.

  3. Give the token to the caller. The caller sends Authorization: Bearer <token>.

  4. To revoke a key, delete its Secret. No AgentRuntime change is needed:

    Terminal window
    kubectl delete secret agent-my-agent-clientkey-terraform -n my-namespace

The key’s claims reach ToolPolicy as principal.claims. A key minted with --role premium is matched by principal.claims has role && principal.claims.role == "premium". A key with no claims gets role set to defaultRole. See Configure tool policies.

To create the Secret without the script, follow the client key Secret contract. The facade skips a Secret whose ownerReferences do not name the AgentRuntime’s UID, and one whose keyHash is not 32 raw bytes. Each skip is logged.

  1. Point the facade at your issuer:

    spec:
    externalAuth:
    oidc:
    issuer: "https://auth.example.com"
    audience: "my-agent"

    issuer must equal the iss claim in your tokens exactly. These are common issuer shapes. Confirm yours in the provider’s discovery document.

    Providerissuer
    Auth0https://<tenant>.auth0.com/
    Oktahttps://<org>.okta.com/oauth2/default
    Googlehttps://accounts.google.com
    Keycloakhttps://<host>/realms/<realm>
    Azure ADhttps://login.microsoftonline.com/<tenant-id>/v2.0
  2. Wait for the controller to mirror the JWKS, then check the condition:

    Terminal window
    kubectl get agentruntime my-agent -n my-namespace \
    -o jsonpath='{.status.conditions[?(@.type=="OIDCJWKSReady")]}'

    The controller creates the Secret agent-my-agent-oidc-jwks and refreshes it every six hours. The facade verifies tokens in-process, so no service mesh is needed.

The IdP’s role and group claims reach ToolPolicy under their own names, for example principal.claims.groups. claimMapping renames only the subject and end-user claims.

A WebSocket connection admitted by an OIDC token closes with code 4401 shortly before the token’s exp. To keep it open, send a new token in a credential_refresh frame before then. See WebSocket protocol.

If your issuer mints a token per conversation, name the claim that identifies the conversation:

spec:
externalAuth:
oidc:
issuer: "https://auth.example.com"
audience: "my-agent"
sessionClaim: diagnostic_session_id

The facade then refuses a token without the claim, and a token can resume only a session that was started with the same claim value. This applies to WebSocket sessions and A2A contexts. An A2A client starts a conversation by sending no contextId, because a contextId the facade has not recorded is refused. The check needs session-api. Without it, every resume is refused. A session that session-api could not record, which the facade then runs unrecorded, cannot be resumed by a bound client either.

  1. Name the headers your edge injects. The defaults match the chart’s Istio layout:

    spec:
    externalAuth:
    edgeTrust:
    headerMapping:
    subject: x-user-id
    endUser: x-user-id
    email: x-user-email
    claimsFromHeaders:
    x-user-groups: groups

    A request is admitted when the subject header is present. The role header x-user-roles is always read into the role claim. To expose other headers, list them in claimsFromHeaders.

  2. Make the edge strip inbound copies of every header in headerMapping and claimsFromHeaders.

managementPlane is set per facade and defaults to true. Set it to false on a facade to remove its management twin, which blocks the dashboard debug view for that facade:

spec:
facades:
- type: websocket
managementPlane: false

The facade reads the credential only from the Authorization header of the WebSocket upgrade request. A browser’s WebSocket constructor cannot set that header, so browser callers connect through a server-side proxy.

Terminal window
websocat -H "Authorization: Bearer $TOKEN" \
"wss://<agent-host>/ws?agent=my-agent"
import WebSocket from 'ws';
const ws = new WebSocket('wss://<agent-host>/ws?agent=my-agent', {
headers: { Authorization: `Bearer ${token}` },
});

See Expose agents for the host.

  1. Confirm the ExternalAuth condition lists your validators:

    Terminal window
    kubectl get agentruntime my-agent -n my-namespace \
    -o jsonpath='{.status.conditions[?(@.type=="ExternalAuth")].message}'

    The message has the form facade admits: clientKeys, managementPlane.

  2. Connect without a token. The upgrade is refused with 401.

  3. Connect with a valid token. The facade sends a connected message.

The facade logs the first rejection of each cause at the default level and repeats at debug level (LOG_LEVEL=debug, set through spec.facades[].extraEnv):

Terminal window
kubectl logs -n my-namespace \
-l app.kubernetes.io/name=omnia-agent,app.kubernetes.io/instance=my-agent \
-c facade --tail=50

Look for auth middleware rejected request. The reasonCode field names the cause, and remedy says what to change:

reasonCodeCause
no_credentialNo Authorization header, or no validator recognised it. Also appears when a management token is sent to the external port
expiredThe key or JWT has expired
bad_signature, unknown_key, bad_algorithm, malformed_tokenThe JWT does not verify against the JWKS. oidc accepts RS256 only
bad_issuer, bad_audienceThe token’s iss or aud differs from issuer or audience
missing_session_claimThe token lacks the claim that oidc.sessionClaim names
Terminal window
kubectl get secret agent-my-agent-oidc-jwks -n my-namespace
kubectl describe agentruntime my-agent -n my-namespace

If the Secret is missing, the OIDCJWKSReady condition gives the reason, for example DiscoveryFailed. Check that the operator pod can reach the issuer. The facade does not start with oidc enabled until the Secret exists. If tokens are still rejected, compare issuer with the iss claim character by character, including any trailing slash.

Tool handlers receive edge-trust claims as X-Omnia-Claim-<Name> headers, and ToolPolicy reads them as principal.claims. If they are missing:

  1. Confirm the edge injects the expected headers, using a debug container or Envoy access logs.
  2. Confirm the edge strips inbound copies of those headers.
  3. Confirm claimsFromHeaders lists the header names as the edge emits them. Header names are case-insensitive.