Skip to main content
Version: 2026.10

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:

To go back, see Switching back to Zitadel. Keep Zitadel running afterwards; see Retiring Zitadel.

Before You Switch​

  1. 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.
  2. Keep Zitadel available. The migration reads existing users and organizations from it.
  3. 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.
  4. 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.
  5. 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 -
import-humans.yaml
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 keycloak is required: a pre-registration is claimed only by a sign-in through the provider it names.
  • Leave ENROLLMENT_POLICY at 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.idpMigration block, including to and fromIssuer, to prevent further migration runs. A leftover to overrides the chart's provider selection.
  • Set identity.oidc.provider: keycloak in values so the chart can read the selected provider.
  • List keycloak-identity-service-env last, 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​

  1. In a private browser window, open https://<customer_istari_fqdn>. The sign-in page is Keycloak's realm.
  2. 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, organizations and consumers authorities are rejected at startup.
  • OIDC_CLIENT_AUTH_METHOD must be client_secret_basic or client_secret_post; private_key_jwt is rejected for Entra.
  • OIDC_PRIVATE_KEY is deliberately empty, for the same reason as for Keycloak.
  • Do not set JWT_DEFAULT_TENANT_ID or identity.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.

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 -
import-principal-mappings.yaml
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.idpMigration block, as for Keycloak.
  • Set identity.oidc.provider: entra in values, as for Keycloak.
  • List entra-identity-service-env last.

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​

  1. In a private browser window, open https://<customer_istari_fqdn>. The sign-in page is login.microsoftonline.com for your directory.
  2. 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​

  1. Complete the Okta or PingFederate application setup. Confirm immutable sub and a trusted verified-email policy. If email trust is unsuitable, use authoritative subject mappings instead.

  2. While the service still runs on Zitadel, make a preparation upgrade using the existing release and image, with this values excerpt (replace okta with pingfederate as appropriate):

    identity:
    oidc:
    provider: zitadel
    idpMigration:
    enabled: true
    from: zitadel
    fromIssuer: https://zitadel.<customer_istari_fqdn>
    to: okta

    Keep Zitadel credentials available and check the imported accounts and counts. Use import-humans for new users, with the incoming provider name.

  3. 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 incoming identity.oidc.provider. Remove conflicting outgoing OIDC_* settings. PingFederate requires client_secret_basic; Okta supports basic or post.

  4. 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​

  1. Complete Google or Lastwall application registration, and export authoritative incoming subjects for each existing person.
  2. 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.to to Google or Lastwall: these are not supported hook targets.
  3. Freeze sign-ins. Run the subject mapping import first with -dry-run, then without it, replacing entra with google or lastwall throughout. Use immutable sub, never email or a guessed ID. For net-new users use the shared subject-based import-humans recipe.
  4. Switch with the incoming provider page's values and credential Secret and identity.idpMigration.enabled: false. Remove conflicting old provider settings. Both providers use openid profile email; Google adds offline access as a protocol parameter.
  5. 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:

  1. Restore all Zitadel identity.oidc settings, or clear incoming overrides to use the Configurator Secret again.
  2. Restore zitadel-identity-service-env, or the manual Zitadel credentials in istari-identity. Remove the incoming provider's Secret and its OIDC_* and provider overrides from identity.env and other mounted Secrets.
  3. Restore the saved import settings; do not leave identity.idpMigration.to pointing at the provider you are leaving. Preserve the database, provisioner's state, Identity Service signing/encryption keys, and platform credentials.
  4. 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​

SymptomCause and fix
The switch upgrade fails in the istari-identity-idp-migrate Job, calling the new IdPidentity.oidc.provider is not set in values. Set it to keycloak or entra.
A later upgrade fails in the migration Job after Zitadel is retiredidentity.idpMigration.enabled is still true from the migration upgrade. Remove it.
The Identity Service does not start after the switch, citing the client auth methodThe 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 ZitadelThe outgoing pod is still stopping (up to 30 seconds). Try again in a private window.
A Keycloak user is refused at sign-inEither 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-inThey 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 AADSTS700027OIDC_CLIENT_AUTH_METHOD is private_key_jwt. Use client_secret_basic with a client secret.