Authentication architecture
Omnia authenticates in two places. The dashboard authenticates people and scripts that manage resources. Each agent facade authenticates the applications that talk to an agent. The two layers share no tokens, no sessions and no configuration. This page explains how each works and why they are separate. For setup, see Configure dashboard authentication and Configure agent authentication.
Authentication layers
Section titled “Authentication layers”The diagram shows who presents what to whom. The dashboard’s pages are static files, and every API call they make goes to management-api, which talks to the facade’s management twin on behalf of a signed-in user. Everything else reaches the facade on its external listener.
graph LR B[Browser] -->|pages| D[Dashboard nginx] B -->|/api, session cookie| M[management-api] S[Script or CI] -->|/api, omnia_sk_ API key| M M -->|mgmt-plane JWT, RS256| MT[Facade management twin] MT --> MV[mgmt-plane validator] C[Client app] -->|Bearer token| EX[Facade external listener] EX --> CH[clientKeys, oidc, edgeTrust]The layers are separate because their callers differ. The dashboard serves humans with browser sessions that last hours. Agents serve applications that hold long-lived keys or IdP-issued tokens, and an organisation may use one identity provider for its staff and another for its customers.
Dashboard authentication
Section titled “Dashboard authentication”The dashboard runs in one of four modes, set by OMNIA_AUTH_MODE. The Helm chart renders that variable from dashboard.auth.mode. The chart has no default for the mode and refuses to render without it, and it refuses anonymous unless dashboard.auth.allowAnonymous is also true. management-api itself falls back to anonymous when the variable is unset, so the chart check prevents an install from running unauthenticated by omission.
Anonymous mode treats every caller as one user with the role in OMNIA_AUTH_ANONYMOUS_ROLE, which defaults to viewer. There is no user identity to attribute actions to, so it suits local development, demos and environments protected at the network layer. Proxy mode delegates authentication to a reverse proxy. OAuth mode talks to an identity provider directly. Builtin mode keeps users in a local database. Programmatic callers use API keys, which work in every mode.
The dashboard’s pages are static files served by nginx, and management-api serves its API. The Gateway or Ingress sends every /api path to management-api, and the dashboard’s nginx proxies any /api request that reaches it, each with the caller’s own session cookie, proxy headers or API key. management-api runs every sign-in route on this page and decides who each API request is from. nginx checks nobody.
Before any mode runs, management-api refuses writes whose Origin is not the dashboard’s own. The session cookie is SameSite=Lax, and browsers still send such a cookie on requests from a sibling subdomain, so the origin check supplies the CSRF defence that the cookie attribute alone cannot. After that, unauthenticated API requests get a 401. In the browser, the page gate asks /api/v1/auth/me and sends a signed-out visitor to /login.
Proxy mode
Section titled “Proxy mode”management-api reads the user name, email, display name and groups from request headers. The defaults are the X-Forwarded-* headers that OAuth2 Proxy sets, and each header name is configurable. The proxy owns the identity provider relationship and management-api owns the session.
sequenceDiagram participant U as User participant P as Reverse proxy participant M as management-api participant S as Session store
U->>P: Request /api P->>M: Forward request with user and group headers M->>M: Resolve role from groups M->>S: Save session record M-->>U: Answer and session cookiemanagement-api trusts those headers exactly as presented. Only the proxy may reach the dashboard and management-api, and the proxy must overwrite any copy of the headers a client sends. In return, the install needs no OAuth client secret, and several applications behind the same proxy get single sign-on.
OAuth mode
Section titled “OAuth mode”management-api drives the authorization-code flow itself, with PKCE. Four provider types exist: generic for any OIDC-compliant provider, google, azure and okta. Each discovers its endpoints from the issuer’s OIDC discovery document.
sequenceDiagram participant U as User participant M as management-api participant S as Session store participant I as Identity provider
U->>M: GET /api/v1/auth/login M->>S: Store PKCE verifier keyed by state M-->>U: Redirect to IdP, state cookie set U->>I: Authenticate I-->>U: Redirect to /api/auth/callback with code U->>M: Callback with code and state M->>S: Consume PKCE record, once M->>I: Exchange code plus verifier I-->>M: ID token and refresh token M->>S: Save session record M-->>U: Redirect to the dashboard, session cookie setPKCE stops an intercepted authorization code from being useful. management-api generates a random verifier, sends only its SHA-256 challenge to the provider, and presents the verifier when it redeems the code. The verifier waits in the server-side store keyed by the state value, and a short-lived cookie binds that state to the browser that started the login. The callback consumes the record exactly once, so a replayed or cross-site callback fails.
OAuth mode gives single sign-out where the provider supports it and refreshes tokens without the user noticing. Whenever management-api resolves a user and the stored token expires within five minutes, it refreshes using the stored refresh token. A failed refresh is logged and the session carries on until its own lifetime ends. The access token itself is not stored.
Builtin mode
Section titled “Builtin mode”Builtin mode needs no identity provider, which suits small teams, air-gapped clusters and proofs of concept. Users live in management-api’s own PostgreSQL database, and the chart refuses a builtin install that names none in managementApi.database.existingSecret.
sequenceDiagram participant U as User participant M as management-api participant DB as User store participant S as Session store
U->>M: POST /api/v1/auth/builtin/login M->>DB: Look up user by email or username M->>M: Check lockout, then bcrypt compare M->>S: Save session record M-->>U: Session cookiePasswords are hashed with bcrypt at 12 rounds. Reset and verification tokens are random, and only their SHA-256 hash is stored. Repeated failures lock the account, five attempts for fifteen minutes by default. A builtin user’s role is part of the user record, and builtin users have no groups.
An install creates users only through two routes. The admin seed runs when username, email and password are all set and the user table is empty. Public signup is off unless OMNIA_BUILTIN_ALLOW_SIGNUP is true. With no admin password and signup off, nobody can log in.
API keys
Section titled “API keys”An API key is an omnia_sk_ string sent as Authorization: Bearer or X-API-Key. When API keys are enabled, management-api tries the key before the session cookie or proxy headers, in every mode. A forged, unknown, expired or revoked key does not authenticate: management-api refuses the request with a 401.
sequenceDiagram participant S as Script or CI participant R as management-api participant K as API key store
S->>R: Request to /api with omnia_sk_ key R->>K: Find key by bcrypt comparison K-->>R: Key with role, owner groups, workspaces R-->>S: Response, 401 for no live key, or 403 outside the key's scopeKeys are stored as bcrypt hashes, and the full key is shown once, at creation. The store is in management-api’s memory by default, read from a mounted Secret in file mode, or PostgreSQL. A memory store loses its keys when management-api restarts, and each replica holds its own, so use PostgreSQL for keys anyone depends on. A key carries a role, an expiry and an optional workspace allowlist, plus a snapshot of its owner’s email and groups so that workspace roles resolve as they would for the owner. management-api records when each key was last used.
Every mode ends in the same two-layer authorization. The platform role covers cross-workspace actions: dashboard settings and managing your own API keys. A role inside a workspace, granted through the Workspace’s roleBindings and directGrants, decides what a person can do with that workspace’s agents, prompt packs, sessions and credentials. A platform editor has no rights beyond a viewer. A platform admin sees every workspace and can manage its access bindings, which is how the admin grants themselves a data role. Until a workspace role is granted, the workspace shows no data to that admin. The dashboard authentication reference lists the permissions of each role.
The platform role comes from the authentication mode, as the diagram shows.
graph LR A[Anonymous] -->|OMNIA_AUTH_ANONYMOUS_ROLE| PR[Platform role] P[Proxy headers] --> G[Groups] O[OAuth ID token claims] --> G G -->|admin groups, then editor groups| PR B[Builtin user record] -->|stored role| PR K[API key] -->|key's role| PR G -->|matched against Workspace roleBindings| WR[Workspace role]For proxy and OAuth users, group membership maps to a role: admin groups are checked first, then editor groups, and everyone else is a viewer. Group names are compared by exact string equality against whatever the provider puts in the group claim. Entra ID, for example, emits group object IDs by default. The how-to guide covers the claim mapping per provider.
Sessions
Section titled “Sessions”The session cookie holds only a session ID, sealed in iron-session’s format with the session secret using AES-256-CBC and an HMAC. The user record, the refresh token, the ID token and the token expiry live in a server-side store: Redis when OMNIA_SESSION_REDIS_URL is set, otherwise memory.
graph LR CK[Cookie: sealed sid only] -->|unseal with session secret| ID[sid] ID --> ST[Session store] ST --> UR[User record] ST --> TK[Refresh token, ID token, expiry] ST -->|Redis URL set| RD[Redis] ST -->|otherwise| MM[Memory, one replica]The cookie is HttpOnly, SameSite=Lax, and Secure in production. Keeping the record on the server has three effects:
- The cookie stays a fixed small size across every provider, whatever the size of the IdP tokens.
- Logout deletes the record, so the session is revoked and not merely forgotten by one browser.
- Every login mints a fresh session ID, which closes session fixation.
Keeping the record on the server makes the session stateful. management-api is the only reader of the records it writes, so one replica can keep them in memory. More than one replica needs a shared Redis, and the chart refuses to render managementApi.replicaCount above 1 without one.
The session secret has to be 32 characters or longer. The Helm chart generates one on first install and keeps it across upgrades, so sessions survive an upgrade. Outside the chart you must supply OMNIA_SESSION_SECRET yourself. In production a cookie-using mode refuses to start without one, because the development fallback is a fixed constant from the public source tree, and it would let anyone who reads the source forge a cookie.
A JWT kept in localStorage is readable by any script on the page and cannot be revoked. A JWT kept in a cookie cannot be revoked either, and large IdP tokens run into the cookie size limit. A sealed session ID with a store behind it allows revocation. Anonymous mode reads no session and needs no store.
Agent authentication
Section titled “Agent authentication”Every facade runs an ordered chain of validators and admits a request when one of them accepts it. When none does, the facade returns 401. A validator that recognises a credential and rejects it, an expired JWT for example, ends the chain at once, so a later validator cannot admit what an earlier one refused.
The facade has two listeners, and each runs its own chain:
graph TB subgraph EXT["External listener"] X1[clientKeys] --> X2[oidc] --> X3[edgeTrust] end subgraph MGT["Management twin listener"] M1[mgmt-plane] end CA[Client app] --> X1 MA[management-api] --> M1 X3 -->|no validator admits| R1[401] M1 -->|invalid token| R2[401]The external listener runs the validators configured under spec.externalAuth, in the order clientKeys, oidc, edgeTrust. With no spec.externalAuth, the chain is empty and every external request gets a 401. The management twin runs only the mgmt-plane validator. It admits RS256 JWTs management-api mints for the dashboard’s signed-in users, verified against management-api’s JWKS endpoint, and it never admits an unauthenticated request. This is how “Try this agent” works without opening the agent to anyone else. The twin exists when the facade’s managementPlane setting is on, and setting it off blocks the dashboard debug view for that facade.
The three data-plane validators cover different trust arrangements:
The clientKeys validator hashes the presented bearer token with SHA-256 and looks the hash up among the agent’s key Secrets, so the cluster never stores the token. An unknown bearer falls through to the next validator, which lets a key and an IdP token coexist. A key carries the claims stored in its Secret. A key with none gets a role claim set to the defaultRole field, which is viewer unless you change it.
The oidc validator verifies RS256 JWTs from your own identity provider, checking issuer, audience and expiry. The AgentRuntime controller fetches the provider’s JWKS into a per-agent Secret and refreshes it every six hours, so the facade verifies tokens in-process and no service mesh is needed. claimMapping renames only the subject and end-user claims. Other claims, such as a role, pass through under their own names.
The edgeTrust validator trusts claim headers that an upstream edge already verified and does not re-verify the token. It admits a request when the subject header is present.
edgeTrust moves the trust boundary to the edge. If the edge fails to strip inbound copies of the claim headers, any caller can claim any identity. When Istio is the edge, the chart can render a RequestAuthentication, an AuthorizationPolicy and an EnvoyFilter for agent pods. That layer is optional and off by default, it is enabled with authentication.enabled and istio.enabled, and the EnvoyFilter strips the spoofable headers before the JWT is checked. It validates the token against the issuer’s JWKS, checks audiences when authentication.jwt.audiences is set, and can copy claims into headers for edgeTrust to read.
The usual choice is clientKeys for a handful of known callers, oidc when end users already hold tokens from your provider, and edgeTrust when a gateway or mesh already terminates the JWT. All three can be enabled together.
Defence in depth
Section titled “Defence in depth”A dashboard API request passes several independent checks in management-api, and failing one stops it:
graph LR RQ[Request] --> OC{Origin check} OC -->|cross-origin write| F1[403] OC --> MW{API key, session or proxy headers} MW -->|none, or one that does not check out| F2[401] MW --> RH[Operation] RH --> PR{Platform role} RH --> WR{Workspace role} PR -->|insufficient| F3[403] WR -->|insufficient| F3The dashboard’s nginx sends the Content-Security-Policy. The default allows 'unsafe-inline' for scripts and styles because Next.js emits inline hydration scripts and runtime-injected styles, and static files carry no per-request nonces. The default also allows 'unsafe-eval' for scripts. A deployment with a tighter policy replaces the whole header through OMNIA_CSP_POLICY on the dashboard container.
Network policy and TLS sit outside these checks. Terminate TLS in front of the dashboard so the secure cookie is sent. Restrict who can reach the dashboard and management-api, and in proxy mode restrict them to the proxy alone.
The chart reads the session secret and the OAuth client credentials from Secret references (existingSessionSecret, existingSecret), and management-api can also read the OAuth client secret from a mounted file. It stores API keys and passwords only as bcrypt hashes.
Related pages
Section titled “Related pages”- Configure dashboard authentication covers modes, providers, role mapping and the session secret.
- Configure agent authentication covers client keys, OIDC and edge-trust recipes.
- Dashboard authentication reference lists every variable, route and permission.
- Policy engine explains how the admitted identity reaches tool policy.