Identity Service: Switching from Zitadel
This guide applies to installations with Zitadel configured as the identity provider. It describes how to switch to another provider while preserving existing Istari accounts and access.
First, prepare the accounts for the new provider while users still sign in with Zitadel. Then change the provider and check that users retain access to their tenants, files, and shares.
Follow the section for your new IdP:
- Switch to Keycloak
- Switch to Microsoft Entra ID
- Switch to Okta or PingFederate
- Switch to Google or Lastwall
To go back, see Switching back to Zitadel. Keep Zitadel running afterwards; see Retiring Zitadel.
Before You Switch
- Set up Identity Service with Zitadel first. If the installation still signs in directly with Zitadel, complete Identity Service setup without changing providers. Confirm that existing users can sign in and access their tenants and files before continuing.
- Keep Zitadel available. The migration reads existing users and organizations from it.
- Back up the database and save your current Helm values. Keep a backup of the Identity Service database and the configuration needed to return to Zitadel if the switch fails.
- Update CLI and agent authentication. If a CLI or agent uses a Zitadel personal access token (PAT), replace it with an Istari key before switching. PATs still depend on Zitadel and the legacy PAT authentication setting.
- Schedule a maintenance period. Users should stay signed out while accounts are linked and the sign-in provider changes. Choose existing users and owned/shared files to check after the switch.
If your values file sets identity.migrations.runAsJob: false, change it to true before continuing. This lets the chart prepare the database before importing accounts; it is already the default.
Apply each upgrade to the existing release with its full values file and the changes shown below.
Switch to Keycloak
People are matched to their Keycloak accounts by verified email: the first time someone signs in through Keycloak, the email on their Keycloak account connects them to their existing Istari account.
1. Register the Identity Service in Keycloak
Complete the Keycloak application setup and record its client ID and secret.
2. Create Keycloak Accounts for Your Users
Every person who should keep their Istari account needs a Keycloak account in that realm with:
- the same email address they have in Zitadel, and
- Email verified turned on.
A person whose Keycloak email is missing, different or unverified is not linked to their existing account when they sign in.
People Who Are New to Istari
Pre-register new users in their tenant before their first sign-in. A verified Keycloak email claims the prepared account. Existing users are prepared by the migration upgrade in step 4.
List them in a CSV with a header row:
email,tenant_slug
jane.doe@example.com,acme
sam.lee@example.com,acme
Load the file into a ConfigMap, then run import-humans as a Job on the Identity Service image, first as a dry run:
kubectl create configmap people-to-pre-register -n istari --from-file=people.csv=people.csv \
--dry-run=client -o yaml | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: import-humans
namespace: istari
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
imagePullSecrets: [{ name: docker-pull-secret }]
containers:
- name: import-humans
image: istaridigital.jfrog.io/customer-docker/identity-service:<tag>
command:
- /import-humans
- -file
- /people/people.csv
- -provider
- keycloak
- -database-url-env
- ISTARI_DIGITAL_IDENTITY_SERVICE_DATABASE_URL
- -dry-run
envFrom:
- secretRef: { name: istari-identity }
volumeMounts:
- { name: people, mountPath: /people, readOnly: true }
volumes:
- name: people
configMap: { name: people-to-pre-register }
kubectl delete job -n istari import-humans --ignore-not-found
kubectl apply -f import-humans.yaml
kubectl logs -n istari -f job/import-humans
Use your deployed Identity Service image tag. Review the dry-run results, remove -dry-run, and rerun the commands. Failed rows fail the Job; skipped rows are counted separately.
- A row is skipped, and counted, when another record already holds the address or it is pre-registered in another tenant; nothing is overwritten. Re-running is safe.
-provider keycloakis required: a pre-registration is claimed only by a sign-in through the provider it names.- Leave
ENROLLMENT_POLICYat its default,open. For restricted enrollment, also configure the Keycloak upstream grouping and tenant mapping.
3. Create the Keycloak Secret
Create the Keycloak Secret. The command reads the client secret without echoing it:
read -rs KEYCLOAK_CLIENT_SECRET # paste the client secret, then press Enter; it is not echoed
printf '%s' "$KEYCLOAK_CLIENT_SECRET" | kubectl create secret generic keycloak-identity-service-env -n istari \
--from-literal=ISTARI_DIGITAL_IDENTITY_SERVICE_IDP_PROVIDER=keycloak \
--from-literal=ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_ISSUER=https://<keycloak_host>/realms/<realm> \
--from-literal=ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_CLIENT_ID=<client_id> \
--from-file=ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_CLIENT_SECRET=/dev/stdin \
--from-literal=ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_CLIENT_AUTH_METHOD=client_secret_basic \
--from-literal="ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_SCOPES=openid profile email offline_access" \
--from-literal=ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_PRIVATE_KEY=
unset KEYCLOAK_CLIENT_SECRET
The empty OIDC_PRIVATE_KEY overrides the Zitadel application key in the earlier-mounted Secret.
The issuer must have no trailing slash. Mount this Secret only during the switch upgrade.
4. Prepare People for Keycloak (Migration Upgrade)
Upgrade with the migration turned on. The Identity Service stays on Zitadel:
identity:
idpMigration:
enabled: true
to: keycloak
fromIssuer: https://zitadel.<customer_istari_fqdn>
The upgrade prepares each person's account for Keycloak. It only adds, and is safe to run again.
5. Switch to Keycloak (Switch Upgrade)
Upgrade again with the migration turned off and the Identity Service pointed at Keycloak. Replace old identity.oidc settings with the Keycloak guide's configuration; nonempty Helm values override the incoming Secret. Add that Secret last:
identity:
oidc:
provider: keycloak
extraEnvSecrets:
- zitadel-identity-service-env
- keycloak-identity-service-env
- Delete the entire
identity.idpMigrationblock, includingtoandfromIssuer, to prevent further migration runs. A leftovertooverrides the chart's provider selection. - Set
identity.oidc.provider: keycloakin values so the chart can read the selected provider. - List
keycloak-identity-service-envlast, so its settings override Zitadel's.
For up to 30 seconds after the upgrade, the outgoing Identity Service pod can still answer and send a sign-in to Zitadel.
6. Verify the Keycloak Switch
- In a private browser window, open
https://<customer_istari_fqdn>. The sign-in page is Keycloak's realm. - Sign in as a migrated user. They see the same tenant, files and roles as before the switch.
Switch to Microsoft Entra ID
Entra accounts are linked by immutable Object ID:
- Existing people are linked before the switch: you pair each person's Zitadel user ID with their Entra Object ID and import the pairs.
- New people are pre-registered by Object ID in their tenant.
Sign-ins without a prepared mapping are refused. Each user keeps their Istari tenant membership.
1. Register the Identity Service in Entra
Complete the Entra application setup. Record the application ID, directory ID, and client secret Value.
2. Create the Entra Secret
read -rs ENTRA_CLIENT_SECRET # paste the client secret value, then press Enter; it is not echoed
printf '%s' "$ENTRA_CLIENT_SECRET" | kubectl create secret generic entra-identity-service-env -n istari \
--from-literal=ISTARI_DIGITAL_IDENTITY_SERVICE_IDP_PROVIDER=entra \
--from-literal=ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_ISSUER=https://login.microsoftonline.com/<directory_tenant_id>/v2.0 \
--from-literal=ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_CLIENT_ID=<application_client_id> \
--from-file=ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_CLIENT_SECRET=/dev/stdin \
--from-literal=ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_CLIENT_AUTH_METHOD=client_secret_basic \
--from-literal="ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_SCOPES=openid profile email offline_access" \
--from-literal=ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_PRIVATE_KEY=
unset ENTRA_CLIENT_SECRET
- The issuer must name your directory's tenant ID. The tenant-independent
common,organizationsandconsumersauthorities are rejected at startup. OIDC_CLIENT_AUTH_METHODmust beclient_secret_basicorclient_secret_post;private_key_jwtis rejected for Entra.OIDC_PRIVATE_KEYis deliberately empty, for the same reason as for Keycloak.- Do not set
JWT_DEFAULT_TENANT_IDoridentity.oidc.defaultTenantId. Entra maps no tenant; the Identity Service ignores the value and logs a warning.
3. Prepare People for Entra (Migration Upgrade)
Upgrade with the migration turned on. The Identity Service stays on Zitadel:
identity:
idpMigration:
enabled: true
to: entra
fromIssuer: https://zitadel.<customer_istari_fqdn>
The upgrade imports current Zitadel users so the next step can link them. It is safe to repeat.
4. Link People to Their Entra Accounts
For each person, pair their Zitadel user ID (the numeric ID on the user's page in the Zitadel console) with their Entra Object ID (on the user's Overview in the Entra admin center), in a CSV:
old_provider,old_subject,new_provider,new_subject,tenant_slug
zitadel,<zitadel_user_id>,entra,<entra_object_id>,<tenant_slug>
One file can cover every tenant: each person keeps their own. tenant_slug is optional; when given, the row is refused unless it names the person's tenant.
For another provider, substitute its name and immutable sub in the CSV and Job's -new-provider argument. The import preserves existing principal IDs.
Freeze sign-ins from the dry run until the import finishes and the cutover is verified, so nothing changes between the check and the write. Load the file and run the importer as a Job, first as a dry run:
kubectl create configmap principal-mappings -n istari --from-file=mappings.csv=principal-mappings.csv \
--dry-run=client -o yaml | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: import-principal-mappings
namespace: istari
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
imagePullSecrets: [{ name: docker-pull-secret }]
containers:
- name: import-principal-mappings
image: istaridigital.jfrog.io/customer-docker/identity-service:<tag>
command:
- /import-principal-mappings
- -file
- /mappings/mappings.csv
- -new-provider
- entra
- -database-url-env
- ISTARI_DIGITAL_IDENTITY_SERVICE_DATABASE_URL
- -dry-run
envFrom:
- secretRef: { name: istari-identity }
volumeMounts:
- { name: mappings, mountPath: /mappings, readOnly: true }
volumes:
- name: mappings
configMap: { name: principal-mappings }
kubectl delete job -n istari import-principal-mappings --ignore-not-found
kubectl apply -f import-principal-mappings.yaml
kubectl logs -n istari -f job/import-principal-mappings
Fix all dry-run errors before writing mappings. Duplicate IDs, missing accounts, and suspended users or tenants are rejected. Remove -dry-run and rerun the commands; check that each row reports linked. Re-running is safe.
People Who Are New to Istari (Entra)
Pre-register each new person in their tenant by their Entra Object ID, with the import-humans Job and -provider entra in place of -provider keycloak. The CSV needs an upstream_subject column holding the Object ID:
email,upstream_subject,tenant_slug
jane.doe@example.com,<entra_object_id>,acme
A row without an upstream_subject is skipped: Entra never claims a pre-registration by email alone. For the same reason, a person added with the web app's Add user cannot sign in through Entra; pre-register them by Object ID instead.
5. Switch to Entra (Switch Upgrade)
Upgrade again with the migration turned off and the Identity Service pointed at Entra. Replace old identity.oidc settings with the Entra guide's configuration; nonempty Helm values override the incoming Secret. Add that Secret last:
identity:
oidc:
provider: entra
extraEnvSecrets:
- zitadel-identity-service-env
- entra-identity-service-env
- Delete the whole
identity.idpMigrationblock, as for Keycloak. - Set
identity.oidc.provider: entrain values, as for Keycloak. - List
entra-identity-service-envlast.
Under Entra, the platform signs in only through the Identity Service's current endpoints. Leave DISCOVERY_API_VERSION unset: 1 prevents startup.
6. Verify the Entra Switch
- In a private browser window, open
https://<customer_istari_fqdn>. The sign-in page islogin.microsoftonline.comfor your directory. - Sign in as a person you linked in step 4. They see the same tenant, files and roles as before the switch.
Switch to Okta or PingFederate
-
Complete the Okta or PingFederate application setup. Confirm immutable
suband a trusted verified-email policy. If email trust is unsuitable, use authoritative subject mappings instead. -
While the service still runs on Zitadel, make a preparation upgrade using the existing release and image, with this values excerpt (replace
oktawithpingfederateas appropriate):identity:oidc:provider: zitadelidpMigration:enabled: truefrom: zitadelfromIssuer: https://zitadel.<customer_istari_fqdn>to: oktaKeep Zitadel credentials available and check the imported accounts and counts. Use
import-humansfor new users, with the incoming provider name. -
Freeze sign-ins, verify mappings, then make the switch upgrade with the provider page's values, incoming credential Secret,
identity.idpMigration.enabled: false, and the incomingidentity.oidc.provider. Remove conflicting outgoingOIDC_*settings. PingFederate requiresclient_secret_basic; Okta supports basic or post. -
Run Verify account continuity before reopening sign-ins. A pending account may be claimed only through the selected provider and trusted verified email; upstream authentication alone does not grant membership.
Switch to Google or Lastwall
- Complete Google or Lastwall application registration, and export authoritative incoming subjects for each existing person.
- While still on Zitadel, rerun the normal Zitadel import if needed and verify every outgoing person has a stable principal and correct local memberships. Keep the source provider running through preparation. Do not set
identity.idpMigration.toto Google or Lastwall: these are not supported hook targets. - Freeze sign-ins. Run the subject mapping import first with
-dry-run, then without it, replacingentrawithgoogleorlastwallthroughout. Use immutablesub, never email or a guessed ID. For net-new users use the shared subject-basedimport-humansrecipe. - Switch with the incoming provider page's values and credential Secret and
identity.idpMigration.enabled: false. Remove conflicting old provider settings. Both providers useopenid profile email; Google adds offline access as a protocol parameter. - Run the continuity checks below. Lastwall has no email enrollment; Google personal accounts require subject imports. Google's restricted Workspace email claiming is not a substitute for subject migration.
Verify account continuity
Before reopening sign-ins, verify with an ordinary user and a separate sharing recipient:
- The principal UUID is unchanged; do not accept a new account with the same email as a successful migration.
- The expected tenant binding and memberships resolve in both Identity Service and Registry. Confirm each user sees the intended tenant and no unintended tenant.
- Existing resources retain their owner and are readable and editable according to the original permissions.
- Existing local tenant/platform roles remain, and an ordinary user has gained no administrator privileges.
- Existing shares still work; create a new share and confirm the recipient can use it. Check recipient lookup independently from resource ownership.
- Existing keys/agents still authenticate and retain access; PAT-only clients were converted before cutover.
- Refresh, logout, and a second login follow the provider-specific policy and keep the same principal.
Stop the rollout if any check fails. Preserve the database backup and source-provider configuration for recovery.
Switching Back to Zitadel
Pause sign-ins and restore the configuration saved before the switch:
- Restore all Zitadel
identity.oidcsettings, or clear incoming overrides to use the Configurator Secret again. - Restore
zitadel-identity-service-env, or the manual Zitadel credentials inistari-identity. Remove the incoming provider's Secret and itsOIDC_*and provider overrides fromidentity.envand other mounted Secrets. - Restore the saved import settings; do not leave
identity.idpMigration.topointing at the provider you are leaving. Preserve the database, provisioner's state, Identity Service signing/encryption keys, and platform credentials. - Upgrade the existing release with the restored values, verify login, tenant membership, and access, then reopen sign-ins.
People keep their accounts: their Zitadel sign-ins still reach the same accounts. The links made for the other IdP stay in place and do nothing while Zitadel is the IdP.
Retiring Zitadel
Keep Zitadel running after the switch. Parts of the platform's user management still read from it, and it is what switching back returns to. Don't retire it while those parts still depend on it. Before you retire it, also check that:
- everyone you migrated has signed in through the new IdP and reached their existing account, and
- no user or agent still signs in with a personal access token.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
The switch upgrade fails in the istari-identity-idp-migrate Job, calling the new IdP | identity.oidc.provider is not set in values. Set it to keycloak or entra. |
| A later upgrade fails in the migration Job after Zitadel is retired | identity.idpMigration.enabled is still true from the migration upgrade. Remove it. |
| The Identity Service does not start after the switch, citing the client auth method | The new IdP's secret is missing OIDC_CLIENT_SECRET or OIDC_CLIENT_AUTH_METHOD, or is listed before zitadel-identity-service-env, whose Zitadel settings then win. |
| Right after the switch, sign-in still goes to Zitadel | The outgoing pod is still stopping (up to 30 seconds). Try again in a private window. |
| A Keycloak user is refused at sign-in | Either they have no Istari account and were not pre-registered, or their Keycloak email does not match their Zitadel email or is not verified. Pre-register them (People who are new to Istari), or fix their Keycloak email and its Email verified setting. |
| An Entra user is refused at sign-in | They were neither linked nor pre-registered by Object ID. Add their row to the CSV and run the link import again, or pre-register them. |
Entra sign-in fails at the token exchange with AADSTS700027 | OIDC_CLIENT_AUTH_METHOD is private_key_jwt. Use client_secret_basic with a client secret. |