Authentication
Union.ai self-hosted deployments use OpenID Connect (OIDC) for user authentication and OAuth 2.0 for service-to-service authorization.
Unlike serverless and BYOC deployments where Union.ai manages authentication for you, self-hosted deployments require you to create and manage OAuth applications in your own identity provider (e.g. Okta, Microsoft Entra ID, Google Workspace, or any OIDC-compliant provider). Union.ai does not provision or manage these applications — you are responsible for their lifecycle, credential rotation, and access policies.
This guide covers authentication for self-hosted deployments where you manage both the control plane and data plane. For self-managed deployments (Union.ai-hosted control plane), authentication is handled automatically via uctl selfserve provision-dataplane-resources and uctl create apikey.
Overview
Self-hosted authentication requires creating five OAuth2 client applications in your own identity provider (plus an optional sixth for CI/CD). Each application serves a different authentication flow:
| # | Application | Type | Grant types | Purpose |
|---|---|---|---|---|
| 1 | Browser login | Confidential (web) | authorization_code, refresh_token, client_credentials |
Console/web UI login |
| 2 | CLI | Public (native) | authorization_code, refresh_token, device_code |
uctl / flytectl CLI authentication via PKCE |
| 3 | Service-to-service | Confidential (service) | client_credentials |
Control plane inter-service communication through NGINX |
| 4 | Operator | Confidential (service) | client_credentials |
Data plane operator, propeller, and cluster-resource-sync authentication to control plane |
| 5 | EAGER | Confidential (service) | client_credentials |
Task pod authentication (EAGER_API_KEY) |
| 6 | CI/CD (optional) | Confidential (service) | client_credentials |
Non-interactive workflow deployment from CI/CD pipelines |
App 6 (CI/CD) is only needed if you deploy workflows from automated pipelines. See the CI/CD integration guide for full setup instructions.
With more than one data plane, it’s best practice to create a separate Operator (App 4) and EAGER (App 5) client for each data plane cluster. Each data plane then authenticates as its own identity — one you can grant cluster-management permission on only its own cluster — and mints its own EAGER_API_KEY. Sharing a single Operator client forces one identity to hold cluster-management rights on every cluster, and sharing a single EAGER client causes rotation interference: each data plane’s key request rotates the shared secret and invalidates the others. Apps 1–3 (and the optional App 6) stay single and control-plane-wide.
Identity provider requirements
You must use an OIDC-compliant identity provider that you manage outside of Union.ai. Any standards-compliant provider will work. Union.ai uses Okta for its internal deployments, but you can use whichever provider your organization already uses.
Your identity provider must support:
- OpenID Connect Discovery —
/.well-known/openid-configurationor/.well-known/oauth-authorization-serverendpoint - Authorization Code flow — for browser and CLI login
- Client Credentials flow — for service-to-service tokens
- PKCE (Proof Key for Code Exchange) — for the CLI public client
- Custom scopes — ability to create an
allscope (or equivalent) - Custom claims — ability to emit
subandpreferred_usernameclaims in access tokens. An identity type claim (identitytypeor equivalent) is recommended for authorization.
Authorization server setup
Create a custom authorization server (or equivalent) in your identity provider. The setup differs by provider:
Create a Custom Authorization Server in Okta:
- Audience:
https://<your-domain>(the control plane ingress domain) - Default scope:
all - Metadata URL:
.well-known/oauth-authorization-server(Okta-specific, not the standardopenid-configuration) - Claims (add as access token claims):
sub— Okta populates this natively. For client_credentials tokens,subequals the app’s Client ID.identitytype— set to"user"for user tokens,"app"for client_credentials tokenspreferred_username— set to the user’s login for user tokens, or the app’s Client ID for app tokens
Register an App Registration in Microsoft Entra ID:
- App ID URI:
api://<app-name>(this becomes the audience) - Metadata URL:
.well-known/openid-configuration(standard OIDC) - Scopes: The
/.defaultscope is used automatically for client_credentials and CLI flows. No custom scopes are required — browser login uses standard OIDC scopes only. - Claims — configure via the app manifest’s
optionalClaims:sub— Entra populates this natively. For client_credentials tokens,subequals the Service Principal Object ID (not the Client ID).idtyp— add as an optional access token claim on the browser login app registration (App 1), since it is the resource server. Emits"app"for client_credentials tokens (maps to theidentitytypeconcept).preferred_username— included by default for user tokens
Entra ID uses sub = Service Principal Object ID for client_credentials tokens, not the Client ID. When configuring trusted identities for service-to-service auth, use the SP Object ID (found in Enterprise Applications, not App Registrations).
Entra ID v2.0 tokens always include the sub claim, so no fallback claim configuration is required. If you need to override the subject resolution (e.g. to use oid or client_id instead of sub), see the
Subject claim requirements section below for the subjectClaimNames fallback chain.
Entra ID scope usage by flow:
- Browser login (authorization_code): standard OIDC scopes only (
profile,openid,offline_access) — the IdP returns a plain ID token - CLI (authorization_code + PKCE):
api://<app-name>/.default - Service-to-service (client_credentials):
api://<app-name>/.default
For other OIDC providers (Keycloak, Authentik, Auth0, etc.):
- Audience:
https://<your-domain>or a custom resource identifier - Metadata URL: Usually
.well-known/openid-configuration - Scopes: Create an
allscope (or use your provider’s default scope) - Claims: Ensure access tokens include:
sub— a stable identifier for the authenticated principalpreferred_username— display name for identity injection- An identity type claim is optional but recommended for authorization
If your IdP cannot emit an identitytype claim, see the
identity type claim requirements section below.
If your IdP’s client_credentials tokens omit the sub claim, configure subjectClaimNames to specify a fallback chain (e.g., ["sub", "client_id", "azp"]).
Identity type claim requirements
Union.ai uses an identity type claim to distinguish human users from service applications. This distinction is required for Union (built-in RBAC) authorization and affects how access control decisions are made.
Your IdP must emit a claim that maps to the identitytype concept, with values that distinguish user tokens from application tokens. The claim name and values are configurable:
| Provider | Claim name | User value | App value | Configuration |
|---|---|---|---|---|
| Okta | identitytype |
"user" |
"app" |
Custom access token claim on authorization server |
| Entra ID | idtyp |
(not emitted) | "app" |
Enable via optional claims in app manifest. Map with identityTypeClaimsForApps: {idtyp: ["app"]} |
| Generic | varies | varies | varies | Configure identityTypeClaimsForApps to map your claim name and values |
Union authorization mode requires identity type resolution. If your IdP cannot emit any claim that distinguishes users from applications, you must either:
- Set
global.USE_EXTERNAL_IDENTITY: true— the platform will infer identity type from the authentication context (e.g., whether the token was issued via authorization_code or client_credentials flow). This works for basic cases but may not cover all scenarios. - Use External authorization mode instead of Union mode — your external authorization server can determine identity type from the JWT payload,
subclaim, or any other token attribute directly, without relying on the platform’s identity type resolution.
Without identity type resolution, Union authorization cannot distinguish user requests from service account requests, which may result in incorrect access control decisions.
Subject claim requirements
Union.ai uses the JWT sub claim as the primary identifier for all callers — users and service accounts alike. This value is used for:
- Authorization decisions — matching callers to roles and permissions
- Trusted identity validation — verifying internal service-to-service callers
- Audit logging — recording who performed each action
- Resource ownership — the “Owned By” relationship in the console
The sub claim value must be stable and unique per principal. If your IdP returns different sub values for the same user across token refreshes, authorization and ownership tracking will break.
The sub claim is critical for all OAuth applications, including service-to-service (App 3), operator (App 4), and EAGER (App 5). If your IdP does not include sub in client credentials tokens, service-to-service authentication will fail with x-user-subject header not found. Verify that all five applications produce tokens with a sub claim before deploying.
Your IdP must emit a sub claim in all access tokens. If your IdP’s client_credentials tokens use a different claim for the caller identity (or omit sub entirely), configure subjectClaimNames to specify a fallback chain:
# In flyte.configmap.adminServer.auth.appAuth.externalAuthServer:
subjectClaimNames:
- sub # Standard OIDC subject (tried first)
- client_id # OAuth2 client ID (common fallback)
- azp # Authorized party (alternative)The platform tries each claim in order and uses the first non-empty value as the caller’s identity.
Provider-specific sub values:
- Okta:
subequals the Client ID for client_credentials tokens and the user’s Okta ID for user tokens. - Entra ID:
subequals the Service Principal Object ID for client_credentials tokens (not the Client ID). Find this in Entra ID > Enterprise Applications > your app > Object ID. - When configuring trusted identities for internal services (e.g.,
INTERNAL_SUBJECT_ID), use the value that your IdP places in thesubclaim — not necessarily the Client ID.
Step 1: Create OAuth2 applications
Application 1: Browser login (Confidential)
Used by the web console for user authentication.
| Property | Value |
|---|---|
| Type | Web (confidential client) |
| Grant types | authorization_code, refresh_token, client_credentials |
| Redirect URI | https://<your-domain>/callback |
| Post-logout redirect URI | https://<your-domain>/logout |
| Scopes | openid, profile, offline_access |
Note the Client ID (set as flyte.configmap.adminServer.auth.userAuth.openId.clientId) and the Client Secret (stored in the dedicated flyteadmin-oidc-client-secret Secret under the key client_secret, read via userAuth.openId.clientSecretFile — see
Step 3).
Application 2: CLI (Public)
Used by uctl and flytectl for CLI-based authentication with PKCE.
| Property | Value |
|---|---|
| Type | Native (public client) |
| Grant types | authorization_code, refresh_token, device_code |
| Redirect URIs | http://localhost:53593/callback, http://localhost:12345/callback |
| PKCE | Required |
| Client authentication | None (PKCE only, no client secret) |
Note the Client ID (set as flyte.configmap.adminServer.auth.appAuth.thirdPartyConfig.flyteClient.clientId).
Application 3: Service-to-service (Confidential)
Used by control plane services (executions, cluster, identity, etc.) to authenticate with each other through NGINX when OIDC is enabled.
| Property | Value |
|---|---|
| Type | Service (confidential client) |
| Grant types | client_credentials |
Note the Client ID (used as INTERNAL_CLIENT_ID) and the Client Secret (stored in Kubernetes secrets).
Application 4: Operator (Confidential)
Used by data plane services (operator, propeller, cluster-resource-sync) to authenticate to the control plane.
| Property | Value |
|---|---|
| Type | Service (confidential client) |
| Grant types | client_credentials |
Note the Client ID (used as AUTH_CLIENT_ID in data plane configuration) and the Client Secret (stored in Kubernetes secrets).
Create one Operator app per data plane cluster (for example <org>-operator-dp-1, <org>-operator-dp-2). Each data plane uses its own Client ID as AUTH_CLIENT_ID, so it authenticates as a distinct identity that can be granted cluster-management permission on only its own cluster.
Application 5: EAGER (Confidential)
Used for task pod authentication. The encoded credentials form the EAGER_API_KEY.
| Property | Value |
|---|---|
| Type | Service (confidential client) |
| Grant types | client_credentials |
Note the Client ID and Client Secret — these are encoded into the EAGER_API_KEY.
Create one EAGER app per data plane cluster. Each data plane’s operator mints its EAGER_API_KEY against its own EAGER client, so a key request from one data plane never rotates or invalidates another’s.
Step 2: Configure control plane
Authentication is configured in the flyte.configmap.adminServer.auth block in your control plane Helm values. This block defines how the admin service validates tokens, which clients are trusted, and how browser login works.
You also need to set a few global variables for service-to-service authentication:
global:
INTERNAL_CLIENT_ID: "<service-to-service-client-id>" # App 3
AUTH_TOKEN_URL: "<token-endpoint-url>" # OAuth2 token endpoint
OIDC_S2S_SCOPE: "" # Leave empty for Okta, set to "api://<app>/.default" for Entra IDThen configure the auth block. Select your identity provider below:
flyte:
configmap:
adminServer:
server:
security:
useAuth: true
auth:
appAuth:
authServerType: External
externalAuthServer:
baseUrl: "https://dev-123456.okta.com/oauth2/default"
metadataUrl: ".well-known/oauth-authorization-server"
allowedAudience:
- "https://<your-domain>"
thirdPartyConfig:
flyteClient:
clientId: "<cli-client-id>" # App 2
redirectUri: "http://localhost:53593/callback"
scopes:
- all
userAuth:
openId:
baseUrl: "https://dev-123456.okta.com/oauth2/default"
clientId: "<browser-login-client-id>" # App 1
scopes:
- profile
- openid
- offline_access
cookieSetting:
sameSitePolicy: LaxMode
domain: "<your-domain>"Set globals:
global:
INTERNAL_CLIENT_ID: "<service-to-service-client-id>"
AUTH_TOKEN_URL: "https://dev-123456.okta.com/oauth2/default/v1/token"
OIDC_S2S_SCOPE: "" # Okta defaults to "all"flyte:
configmap:
adminServer:
server:
security:
useAuth: true
auth:
appAuth:
authServerType: External
externalAuthServer:
baseUrl: "https://login.microsoftonline.com/<tenant-id>/v2.0"
metadataUrl: ".well-known/openid-configuration"
allowedAudience:
- "api://<app-name>"
- "<browser-login-client-id>" # App 1 Client ID
identityTypeClaimsForApps:
idtyp:
- app
thirdPartyConfig:
flyteClient:
clientId: "<cli-client-id>" # App 2
redirectUri: "http://localhost:53593/callback"
scopes:
- "api://<app-name>/.default"
audience: "api://<app-name>"
userAuth:
openId:
baseUrl: "https://login.microsoftonline.com/<tenant-id>/v2.0"
clientId: "<browser-login-client-id>" # App 1
scopes:
- profile
- openid
- offline_access
cookieSetting:
sameSitePolicy: LaxMode
domain: "<your-domain>"
idpQueryParameter: "idp"After creating service apps (Apps 3-5), you must grant admin consent for their App Role assignments in the Azure portal (Enterprise Applications > your app > Permissions > Grant admin consent) or via az ad app permission admin-consent. Without admin consent, client_credentials token requests will fail.
Set globals:
global:
INTERNAL_CLIENT_ID: "<service-to-service-client-id>"
AUTH_TOKEN_URL: "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token"
OIDC_S2S_SCOPE: "api://<app-name>/.default"INTERNAL_SUBJECT_ID defaults to INTERNAL_CLIENT_ID for backward compatibility. For Entra ID, where the token sub claim is the Service Principal Object ID (not the Client ID), set INTERNAL_SUBJECT_ID to the SP Object ID. Find this in Entra ID > Enterprise Applications > your app > Object ID.
flyte:
configmap:
adminServer:
server:
security:
useAuth: true
auth:
appAuth:
authServerType: External
externalAuthServer:
baseUrl: "<issuer-url>"
metadataUrl: ".well-known/openid-configuration"
allowedAudience:
- "<audience>"
thirdPartyConfig:
flyteClient:
clientId: "<cli-client-id>" # App 2
redirectUri: "http://localhost:53593/callback"
scopes:
- all
userAuth:
openId:
baseUrl: "<issuer-url>"
clientId: "<browser-login-client-id>" # App 1
scopes:
- profile
- openid
- offline_access
cookieSetting:
sameSitePolicy: LaxMode
domain: "<your-domain>"Set globals:
global:
INTERNAL_CLIENT_ID: "<service-to-service-client-id>"
AUTH_TOKEN_URL: "<issuer-url>/token" # Your IdP's token endpoint
OIDC_S2S_SCOPE: "" # Set if your IdP requires a specific scope for client_credentialsIf your IdP’s client_credentials tokens don’t include a sub claim, add:
subjectClaimNames:
- sub
- client_id
- azpSetting useAuth: true is required for the /login, /callback, and /me endpoints to register. Without this, auth endpoints will return 404.
Step 3: Create Kubernetes secrets (control plane)
The control plane needs secrets for the browser login app (App 1) and the service-to-service app (App 3):
# Browser-login (App 1) OIDC client secret, in its own dedicated secret.
# flyteadmin reads it via userAuth.openId.clientSecretFile (configured below).
kubectl create secret generic flyteadmin-oidc-client-secret \
--from-literal=client_secret='<BROWSER_LOGIN_CLIENT_SECRET>' \
-n <controlplane-namespace>
# Add service-to-service client secret to the controlplane secrets
kubectl create secret generic <controlplane-secrets> \
--from-literal=pass.txt='<DB_PASSWORD>' \
--from-literal=client_secret='<SERVICE_TO_SERVICE_CLIENT_SECRET>' \
-n <controlplane-namespace> --dry-run=client -o yaml | kubectl apply -f -Point flyteadmin at the browser-login secret by mounting it and setting clientSecretFile in your control plane values (see the chart’s examples/values.oidc-client-secret.yaml):
flyte:
flyteadmin:
# additionalVolumes / additionalVolumeMounts are list-valued (Helm replaces,
# not merges) — keep any chart-default entries you rely on alongside this one.
additionalVolumes:
- name: oauth-client-secret
secret:
secretName: flyteadmin-oidc-client-secret
additionalVolumeMounts:
- name: oauth-client-secret
mountPath: /etc/secrets/oauth
readOnly: true
configmap:
adminServer:
auth:
userAuth:
openId:
clientSecretFile: /etc/secrets/oauth/client_secretflyteadmin’s flyte-admin-secrets (token-signing and cookie keys) is created automatically by an init container — you don’t create it, and the browser-login client secret does not go there. Keeping the OIDC client secret in its own dedicated secret avoids colliding with those auto-generated keys.
For production, use External Secrets Operator or a similar tool to sync secrets from your cloud provider’s secret manager (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault).
Step 4: Configure data plane
Repeat this step for each data plane, using that data plane’s own Operator (App 4) client ID and secret. Every data plane gets its own AUTH_CLIENT_ID and its own union-secret-auth secret in its namespace.
Add the operator client ID to your data plane overrides file:
global:
AUTH_CLIENT_ID: "<operator-client-id>" # App 4Create the data plane auth secret:
kubectl create secret generic union-secret-auth \
--from-literal=client_secret='<OPERATOR_CLIENT_SECRET>' \
-n <dataplane-namespace>Step 5: Configure EAGER_API_KEY
The EAGER_API_KEY is a base64-encoded string containing the EAGER app credentials. It enables task pods to authenticate to the control plane.
Generate a distinct EAGER_API_KEY for each data plane from that data plane’s own EAGER (App 5) credentials, and create the secret in that data plane’s namespace.
Generate the key:
# Format: base64("<domain>:<eager-client-id>:<eager-client-secret>:")
echo -n "<your-domain>:<eager-client-id>:<eager-client-secret>:" | base64Create the Kubernetes secret in the data plane namespace:
kubectl create secret generic <eager-secret-name> \
--from-literal=<eager-secret-key>='<BASE64_ENCODED_EAGER_API_KEY>' \
-n <dataplane-namespace>The exact secret name and key depend on your deployment’s embedded K8s secret manager configuration. The secret name is typically an MD5 hash of a logical identifier. Contact Union.ai support for the exact values for your organization.
Provision the task-pod key automatically with the operator
Rather than encoding the EAGER_API_KEY and creating the data plane secret by hand, you can let the data plane operator mint it and write it to the task-pod secret store, where the pod webhook injects it into task pods. Enable it in the data plane Helm values:
config:
operator:
apiKey:
enabled: trueThis relies on the embedded Kubernetes secret manager (proxy.secretManager.enabled, enabled by default). The operator scopes the key to its own cluster, so in a multi-data-plane organization each data plane provisions and rotates its own EAGER_API_KEY without overwriting another’s. With this enabled you skip the manual base64 encoding and kubectl create secret steps above.
config.operator.apiKey.enabled is currently opt-in (default false) and is expected to become the default in a future chart release. It still requires the EAGER (App 5) credentials to be resolvable on the control plane — either a registered EAGER client or a seeded apiKeyOverrides entry (see below).
Seed EAGER credentials without creating an OAuth app (apiKeyOverrides)
Some identity providers cannot create service (client_credentials) OAuth applications programmatically, or you may run a control plane with no identity-provider integration at all. In that case the control plane cannot register an EAGER client on demand when a task pod requests its key.
Instead of registering an app, seed the EAGER client credentials as a Kubernetes Secret and point the identity service at it with apiKeyOverrides. When a key request matches an override, the identity service returns the seeded credentials from the mounted Secret rather than creating a new OAuth application in your IdP.
Configure overrides in your control plane Helm values:
services:
identity:
apiKeyOverrides:
- key: EAGER_API_KEY
existingSecret:
name: eager-default-creds # no clusterName — control-plane-wide fallback
- key: EAGER_API_KEY
clusterName: dp-1 # this override applies only to the dp-1 data plane
existingSecret:
name: eager-dp-1-creds # Secret holding the seeded EAGER client credentials
- key: EAGER_API_KEY
clusterName: dp-2
existingSecret:
name: eager-dp-2-credskey— the system key to override (EAGER_API_KEY).clusterName(optional) — scopes the override to one data plane. A key request resolves on(organization, key, clusterName), preferring the entry that matches the requesting data plane’s cluster and falling back to a nameless entry (the control-plane-wide default, shown first above). With multiple data planes, give each its ownclusterNameentry and seeded Secret so their credentials stay independent.existingSecret.name— a Secret you create in the control plane namespace holding the seeded EAGER client’s ID and secret. By default the chart reads the keysclient_idandclient_secret; setclientIdKey/clientSecretKeyif your Secret uses different keys.
Create one seeded Secret per data plane:
kubectl create secret generic eager-dp-1-creds \
--from-literal=client_id='<EAGER_CLIENT_ID>' \
--from-literal=client_secret='<EAGER_CLIENT_SECRET>' \
-n <controlplane-namespace>With an override in place you do not create App 5 in your IdP for that data plane — the seeded credentials serve its EAGER_API_KEY directly.
Step 6: Deploy
Deploy or upgrade both the control plane and data plane with the updated configurations:
# Upgrade control plane
helm upgrade unionai-controlplane unionai/controlplane \
--namespace <controlplane-namespace> \
-f values.<cloud>.yaml \
-f my-overrides.yaml \
--skip-crds --timeout 15m --wait
# Upgrade data plane
helm upgrade unionai-dataplane unionai/dataplane \
--namespace <dataplane-namespace> \
-f values.<cloud>.yaml \
-f dataplane-overrides.yaml \
--skip-crds --timeout 10m --waitVerification
# Check admin service logs for auth initialization
kubectl logs -n <controlplane-namespace> deploy/<admin-service> | grep -i auth
# Test the /me endpoint (should return 401 without a token)
kubectl exec -n <controlplane-namespace> deploy/<admin-service> -- \
curl -s -o /dev/null -w "%{http_code}" \
https://<controlplane-ingress>.<controlplane-namespace>.svc.cluster.local/me -k
# Test CLI login
uctl config init --host https://<your-domain>
uctl get project
# Check data plane operator auth
kubectl logs -n <dataplane-namespace> -l app.kubernetes.io/name=operator --tail=50 | grep -i "token\|auth"Summary of secrets
| Secret name | Namespace | Keys | Source |
|---|---|---|---|
flyteadmin-oidc-client-secret |
<controlplane-namespace> |
client_secret |
Browser login app (App 1) secret |
flyte-admin-secrets |
<controlplane-namespace> |
token-signing / cookie keys | Auto-generated by the flyteadmin init container (do not create) |
<controlplane-secrets> |
<controlplane-namespace> |
pass.txt, client_secret |
DB password, Service-to-service app (App 3) secret |
union-secret-auth |
<dataplane-namespace> |
client_secret |
Operator app (App 4) secret |
| EAGER secret | <dataplane-namespace> |
varies | EAGER app (App 5) encoded key |
union-secret-auth and the EAGER secret are per data plane — each data plane holds its own copy in its own namespace, sourced from that data plane’s Operator (App 4) and EAGER (App 5) clients. If instead you seed EAGER credentials via apiKeyOverrides (see the section above), those Secrets live in the control plane namespace.
Self-hosted vs. self-managed authentication
| Aspect | Self-hosted | Self-managed |
|---|---|---|
| OAuth app creation | Manual — create all 5 apps | Automatic — uctl selfserve provision-dataplane-resources creates apps 1-3 |
| EAGER_API_KEY | Manual — encode and create secret | Automatic — uctl create apikey generates and provisions |
| Control plane auth | Configure via Helm values | Managed by Union.ai |
| Data plane auth | Configure AUTH_CLIENT_ID and secret |
Provisioned by uctl selfserve |
Troubleshooting
Admin service auth endpoints return 404
Ensure useAuth: true is set under flyte.configmap.adminServer.server.security. Without this, the /login, /callback, and /me endpoints are not registered.
Token validation fails with “audience mismatch”
The allowedAudience in the admin service configuration must include https://<your-domain>. This should match the audience configured on your authorization server.
Data plane cannot authenticate to control plane
# Verify AUTH_CLIENT_ID is set
kubectl get configmap -n <dataplane-namespace> -o yaml | grep -i auth_client
# Check that union-secret-auth exists
kubectl get secret union-secret-auth -n <dataplane-namespace> \
-o jsonpath='{.data.client_secret}' | base64 -d
# Check operator logs
kubectl logs -n <dataplane-namespace> -l app.kubernetes.io/name=operator --tail=50 \
| grep -i "auth\|token\|401"CLI login fails
Ensure the CLI app (App 2) redirect URIs include http://localhost:53593/callback and PKCE is enabled. Test with:
uctl config init --host https://<your-domain>
uctl get projectEntra ID: AADSTS1002012 invalid_scope for service-to-service
Client_credentials flows in Entra ID require the /.default scope. Ensure OIDC_S2S_SCOPE is set to api://<app-name>/.default in your globals.
Subject not found in token
If flyteadmin logs show subject claim not found, your IdP’s client_credentials tokens may not include a sub claim. Configure subjectClaimNames in the auth block to specify a fallback chain (e.g., ["sub", "client_id"]).