Skip to main content
Version: 2026.09

Identity Service: Clients & Tenants

Once the Identity Service is deployed (Scenario 10), it needs to know the platform services that sign in through it (its clients) and the tenants your people and agents belong to. In a chart install with the provisioner, as in Scenario 10, both happen during the install or upgrade. This page explains what happens, what is left for you, and how to check it.

With the provisioner (Scenario 10)Without the provisioner
Registry, frontend and MCP serviceNothing to do: the provisioner generates their credentials, and the Identity Service registers them at startupGenerate the registry's credential and set the variables
TenantsYour existing Zitadel organizations become tenants through the chart's Zitadel import; after that, create tenants in the web app or with create-tenant, never in ZitadelThe same
Secure Connection Service agentOnly if you use Secure Connections: add it to your values; see If You Use the Secure Connection ServiceThe same
info

Agents are not registered here. Agents are provisioned through the platform — the Generate Key action in the admin Agents page, or the CLI key commands and SDK — which creates both the Identity Service key and the matching agent identity in the registry service.

The Secure Connection Service's agent is the exception, registered by the chart when you add it to your values; see If You Use the Secure Connection Service.

How the Platform Clients Are Registered​

The Identity Service is itself a client of your identity provider (Zitadel Registration), and the platform's own services are in turn clients of the Identity Service, each under a fixed client ID: registry, frontend and mcp. The registry is a confidential client: it holds an ECDSA P-384 key pair and signs in with signed JWT assertions (RFC 7523). The frontend and MCP service are browser-based public clients with no key; each is a client ID plus an exact-match allowlist of redirect URIs. The Identity Service stores only the registry's public key; the private key stays in the registry's Secret.

At every startup, before it serves requests, the Identity Service reads one environment variable per client and registers that client, or replaces its key or allowlist:

VariableRegisters
ISTARI_DIGITAL_IDENTITY_SERVICE_REGISTRY_PUBLIC_KEY_B64The registry service, a confidential client, with this public key. It can provision agents and look up principals
ISTARI_DIGITAL_IDENTITY_SERVICE_FRONTEND_REDIRECT_URISThe frontend, a public client, with this redirect allowlist
ISTARI_DIGITAL_IDENTITY_SERVICE_MCP_REDIRECT_URISThe MCP service, a public client, with this redirect allowlist

All three are required: the Identity Service does not start without them, or with a malformed value. It never removes a client.

Where the values come from depends on how you install:

  • Chart install with the provisioner (provisioner.enabled: true): you supply none of them, as described below.
  • Chart install with the provisioner in a separate release (provisioner.external: true here, with the same fullnameOverride in both releases): the same as with the provisioner. This release mounts the Secrets the other release's provisioner writes.
  • Chart install without the provisioner, or no chart: you generate the registry's credential and set the variables yourself; see Supplying the Credentials Yourself.

With the provisioner:

  • The provisioner (provisioner.enabled: true) generates the registry's key pair and the MCP service's own client secret, and writes them to Secrets the services mount: the registry's credential to istari-provisioner-registry-credentials, the MCP service's to istari-provisioner-mcp-credentials, and the registry's public key to istari-provisioner-identity-platform-clients for the Identity Service. The private key never reaches the Identity Service. The MCP secret is a setting of the MCP service itself: the Identity Service registers the MCP service as a public client, with no secret, and never checks it. The provisioner runs as a Job on install and whenever its inputs change; the registry, MCP service and Identity Service do not start until it has written their Secrets, which normally takes under a minute.
  • The chart derives the redirect allowlists from common.mainFqdn: https://<mainFqdn> for the frontend, and https://mcp.<mainFqdn>/auth/callback for the MCP service (or the host in common.mcpFqdnOverride).
  • The services use the fixed client IDs by default. The frontend has no credential, so it mounts no provisioner Secret.

The generated Secrets are named after the release's fullnameOverride (istari by default). The Istari Platform chart's README describes the provisioner's settings.

Overriding the redirect allowlists​

To allow other or additional redirect URIs, set ISTARI_DIGITAL_IDENTITY_SERVICE_FRONTEND_REDIRECT_URIS or ISTARI_DIGITAL_IDENTITY_SERVICE_MCP_REDIRECT_URIS in the istari-identity secret. A value in that secret wins over the chart's, and replaces it rather than adding to it, so list every URI the client uses. Values are comma-separated and exact-match: no wildcards or prefixes, and no trailing-slash forgiveness. Restart the Identity Service to apply a change. Because Scenario 10 turns on ENFORCE_CLIENT_REGISTRATION, a sign-in that returns to an address missing from the allowlist is refused; add every address people use to reach the frontend and MCP service.

Tenants​

A tenant is the Identity Service's own grouping of principals — every agent and user belongs to one. Tenants belong to the Identity Service, not to Zitadel: when you enable it, the chart's Zitadel import turns your existing Zitadel organizations into tenants, and after that you create tenants in the web app or with create-tenant rather than in Zitadel. Tenants and their memberships carry over if you change identity provider; Zitadel organizations do not. Each tenant has:

  • slug — the tenant's stable, unique identifier inside the Identity Service (e.g. acme, flight-dynamics). Lowercase, short, permanent: it appears in issued tokens (tenant_slug) and CLI/registry references. It is not the Zitadel organization ID and not shown to end users.
  • display-name — the human-readable label shown in UIs. Free-form, changeable, no uniqueness requirement.
  • provider mapping (optional) — the Zitadel organization the import created the tenant from. A tenant you create yourself needs none.

Tenants from the chart's Zitadel import​

From Istari Platform chart 6.2.0, a chart install that uses Zitadel (the default) creates these tenants for you, by importing your Zitadel organizations on install and on every upgrade. The import reads Zitadel with the Zitadel management key, a key for a Zitadel service user. For each organization that service user is a member of, the import:

  • creates a tenant if the organization has none, with the organization's name as its display name and the organization as its provider mapping;
  • derives that tenant's slug from the organization's name (Acme Corp becomes acme-corp), adding -2, -3, and so on when another tenant has the slug. List the tenants to see the slug each one received;
  • creates an Identity Service user for each of the organization's people, so they sign in to its tenant. People who hold Zitadel's customer_admin role become tenant administrators.

The import records itself as done once it completes without skipping a person's role grant. Later installs and upgrades against the same Zitadel instance then change nothing. An organization that the service user was not a member of when the import was recorded as done needs its tenant created by hand.

What the import needs, and how to turn it off, are in the prerequisites for enabling the Identity Service.

Edge case: skipped role grants

The import skips a role grant, and logs a warning, in two cases:

  • The grant names a person who has no Identity Service user. Usually the person belongs to an organization the service user is not a member of: a Zitadel organization can grant a role to someone from another organization, but the import creates Identity Service users only for people in the service user's organizations.
  • The grant names a Zitadel machine user. The import skips these grants on every run, and they do not keep the import from being recorded as done.

A skipped grant to a person keeps the import from being recorded as done, so the next install or upgrade runs it again. Until a run imports the grant, the person does not hold that role in the Identity Service.

The install or upgrade still continues, unless that run imported no grant and found none already assigned (a revoked one counts). Then the import fails, and so does the install or upgrade.

To import a person's skipped grant, you add the management key's service user to the person's organization. The import then also creates a tenant for that organization, and Identity Service users for its people.

  1. If the chart has deleted the import Job, set identity.idpMigration.autoCleanupSuccessfulJob: false and run the upgrade again, so the chart keeps the Job it runs next.
  2. In the Job's log, find the person's Zitadel user ID. Look up their organization in Zitadel.
  3. Add the management key's service user to that organization. The read-only ORG_OWNER_VIEWER role is enough.
  4. Run the same helm upgrade --install command again.

The import runs only when the Identity Service's identity provider is Zitadel. If you run another provider and configure it only in the identity secret, the chart still assumes Zitadel, so also set identity.oidc.provider, or the provider entry in identity.env.

Import settings and versions​

SettingEffect
identity.idpMigration.enabledOn by default from chart 6.2.0; false turns the import off.
identity.tagThe Identity Service image. Use 2.1.0, the chart's default from istari-platform 6.4.0.
identity.idpMigration.autoCleanupSuccessfulJobfalse keeps the import Job after it succeeds, so you can read its skipped-grant warnings with kubectl logs job/istari-identity-idp-migrate (for a release named istari). Otherwise the chart deletes the Job when it succeeds.
ISTARI_DIGITAL_IDENTITY_SERVICE_ZITADEL_MANAGER_KEYThe Zitadel management key, in istari-identity or zitadel-identity-service-env. The import fails without it.
Identity Service imageNeeded for
2.0.0-pre.17 or laterThe import. An older image fails the import, and with it the install or upgrade, or imports every organization the key can see instead of only those its service user is a member of.
2.0.0-pre.18 or laterplatformRoles in an agent entry. An older image fails agent registration.
2.0.1 or laterAn agent entry without tenantSlug. An older image fails agent registration.

The chart takes the identity provider from the first of these that is set:

  1. identity.idpMigration.to
  2. an ISTARI_DIGITAL_IDENTITY_SERVICE_IDP_PROVIDER entry in identity.env
  3. identity.oidc.provider
  4. otherwise, Zitadel

Upgrading from a chart older than 6.2.0 with --reuse-values alone keeps the old chart's values:

  • identity.tag keeps the old image (1.2.3 in charts 5.5.0 to 6.1.0), which is too old for the import and for the Secure Connection Service agent entry, so the upgrade fails.
  • On charts 5.9.0 to 6.1.0, identity.idpMigration.enabled: false also stays, so the import does not run.

Use --reset-then-reuse-values instead, or set identity.tag to 2.1.0 and identity.idpMigration.enabled: true alongside --reuse-values.

Listing the tenants​

To see which Zitadel organizations have a tenant, and each tenant's slug, query the Identity Service database from a one-off pod. The tenant API cannot show this on a new install: it lists every tenant only to a Platform Administrator, and a new install has none yet. The pod reads DATABASE_URL from the identity secret:

kubectl run list-tenants --rm -i --restart=Never \
--image=postgres:15-alpine \
--overrides='{
"spec":{
"containers":[{
"name":"list-tenants",
"image":"postgres:15-alpine",
"command":["psql"],
"args":["$(DATABASE_URL)","-c",
"SELECT t.slug, t.display_name, m.provider_name, m.provider_tenant_id FROM identity_router.tenants t LEFT JOIN identity_router.tenant_provider_mappings m ON m.tenant_id = t.id"],
"env":[{"name":"DATABASE_URL","valueFrom":{"secretKeyRef":{
"name":"istari-identity",
"key":"ISTARI_DIGITAL_IDENTITY_SERVICE_DATABASE_URL"}}}]
}]}
}'
Clusters without Docker Hub access

Any image that includes psql works, for example a copy of postgres:15-alpine in your own registry. Change the image name in both --image and the --overrides JSON. If your registry needs credentials, also add imagePullSecrets to the pod spec in the --overrides JSON.

Each row is a tenant, with the Zitadel organization the import created it from; a tenant you created yourself shows no organization. An organization you expected but don't see was not imported: create a tenant for its people instead.

An empty list after an install means the import mapped no organization, usually because the management key's service user was a member of none. Upgrading again does not fix this, because an import that finds nothing still counts as done. Create the tenants by hand.

Creating a tenant​

With the Identity Service enabled, create tenants in the Identity Service, not by adding Zitadel organizations. There are two ways:

  • In the web app. A Platform Administrator clicks New tenant on the Platform Admin Console's Tenants page and enters a display name and slug; see Create a tenant.
  • With create-tenant, for when nobody can sign in to the console yet. Run it as a one-off pod (DATABASE_URL comes from the identity secret; you never type it):
kubectl run create-tenant --rm -i --restart=Never \
--image=istaridigital.jfrog.io/customer-docker/identity-service:<tag> \
--overrides='{
"spec":{
"imagePullSecrets":[{"name":"docker-pull-secret"}],
"containers":[{
"name":"create-tenant",
"image":"istaridigital.jfrog.io/customer-docker/identity-service:<tag>",
"command":["/create-tenant"],
"args":["-database-url","$(DATABASE_URL)",
"-slug","<tenant_slug>",
"-display-name","<Human-Readable Name>"],
"env":[{"name":"DATABASE_URL","valueFrom":{"secretKeyRef":{
"name":"istari-identity",
"key":"ISTARI_DIGITAL_IDENTITY_SERVICE_DATABASE_URL"}}}]
}]}
}'

Re-running with the same slug is safe. Both ways create the same tenant, with no Zitadel organization. There is no per-tenant client registration: the platform clients above cover all tenants.

Adding people to a tenant. Pre-register them in it with the import-humans Job described in People Who Are New to Istari; a one-row CSV adds a single person. Their first sign-in claims the pre-registration and puts them in its tenant. Set -provider to the identity provider the installation signs in through now:

  • Zitadel: -provider zitadel. Each person also needs a Zitadel account with the same email, Email Verified checked and an initial password (Creating New Users); the default organization is fine.
  • Keycloak: -provider keycloak, with a Keycloak account that has the same email and Email verified on.
  • Microsoft Entra ID: -provider entra, with each person's Object ID in an upstream_subject column; see People Who Are New to Istari (Entra). Their email is not used to match them.
note

The web app's Add user cannot yet pre-register into a tenant created this way: it refuses with tenant_not_mapped, because the tenant has no Zitadel organization. Use import-humans until that is fixed.

If You Use the Secure Connection Service​

tip

Skip this unless your installation uses Secure Connections. The Secure Connection Service signs in as an agent, and the chart registers that agent only when you add it to your values; nothing registers it by default:

istari-values.yaml (excerpt)
identity:
agentRegistration:
enabled: true
extraEnvSecrets:
- zitadel-identity-service-env # also needed here
agents:
- name: secure-connection
keyEnv: ISTARI_DIGITAL_IDENTITY_SERVICE_SCS_AGENT # you add this
usernameEnv: ISTARI_DIGITAL_IDENTITY_SERVICE_SCS_AGENT_USERNAME # from the Configurator
providerName: zitadel
providerTenantIdEnv: ISTARI_DIGITAL_IDENTITY_SERVICE_SCS_AGENT_PROVIDER_TENANT_ID # from the Configurator
displayName: Secure Connection Service Agent
platformRoles:
- secure_connector

Add the agent's public-only credential blob to the istari-identity secret as ISTARI_DIGITAL_IDENTITY_SERVICE_SCS_AGENT; the other two variables come from the Zitadel Configurator's secret (Configurator 1.9.0 or later), which is listed here as well as in identity.extraEnvSecrets because the registration Job mounts only istari-identity and the secrets in its own list. The agent registers in the tenant the Zitadel import created for its organization. The Istari Platform chart's README lists every field of an entry.

Verify a Registration​

Easiest — through the platform, no extra tooling:

  1. Log in at https://<customer_istari_fqdn> — a successful login proves the frontend's public-client registration and the whole browser flow.
  2. Generate a key from Developer Settings — Keys or create an agent from the admin Agents page — success proves the registry's client registration, its right to provision agents, and the tenant mapping in one step.

Thorough — token-level check with the Istari CLI: a registered principal exchanges a signed assertion for an Identity Service token:

stari client init "https://api.<customer_istari_fqdn>" --identity-service \
--credentials-file <path_to_key_file> --yes

then run any CLI command. A successful exchange returns a token whose claims identify the principal: its kind (principal_kind), its id (principal_id) and its tenant (tenant_slug). See Get started with the CLI for installation.

Supplying the Credentials Yourself​

This section applies only without the provisioner — a chart install with provisioner.enabled: false (the chart's default), or an installation that does not use the chart — you generate the registry's credential and give each service its values. Nothing else registers the clients, and the Identity Service does not start until its three platform-client variables are set.

Generate the registry's credential​

  1. Run the generator as a one-off pod on the Identity Service image. It needs no database access:

    kubectl run gen-client-credentials --quiet --rm -i --restart=Never \
    --image=istaridigital.jfrog.io/customer-docker/identity-service:<tag> \
    --overrides='{"spec":{"imagePullSecrets":[{"name":"docker-pull-secret"}]}}' \
    --command -- /gen-client-credentials --stdout | grep -A5 '^{'

    Replace <tag> with Identity Service 2.1.0. Its registry_secret is a base64 credential blob for the client ID registry, and holds the registry's private key. Ignore the other two fields: the client IDs are fixed.

  2. Derive the registry's public key from it, for the Identity Service:

    echo "<registry_secret>" | base64 -d | jq -r .key | openssl pkey -pubout | base64 | tr -d '\n'

A credential generated by an Identity Service release before 2.0.1 names a client ID with a suffix (for example registry-a1b2), and the Identity Service no longer registers that client. Generate a new one.

Chart install without the provisioner​

Add these keys to the services' own secrets. The chart still renders the API URL and the identity-service switch for each service from common.mainFqdn and identity.clientIntegration.enabled, but not the redirect allowlists, which it derives only for the provisioner.

SecretKeys
istari-identityISTARI_DIGITAL_IDENTITY_SERVICE_REGISTRY_PUBLIC_KEY_B64=<public key from step 2>, ISTARI_DIGITAL_IDENTITY_SERVICE_FRONTEND_REDIRECT_URIS=https://<customer_istari_fqdn>, ISTARI_DIGITAL_IDENTITY_SERVICE_MCP_REDIRECT_URIS=https://mcp.<customer_istari_fqdn>/auth/callback
istari-fileserviceISTARI_DIGITAL_IDENTITY_SERVICE_CLIENT_CREDENTIALS=<registry_secret from step 1>
istari-mcpISTARI_DIGITAL_IDENTITY_SERVICE_CLIENT_SECRET=<a generated value, e.g. openssl rand -hex 32> (if the MCP service is deployed)

The frontend needs nothing: it uses the client ID frontend by default. The Identity Service needs the MCP allowlist even when the MCP service is not deployed. Then run helm upgrade --install and restart the Identity Service, registry, frontend and MCP service.

Installations not using the chart​

Set these variables, then restart the Identity Service, followed by the registry, frontend and MCP service:

ServiceVariables
Identity ServiceISTARI_DIGITAL_API_URL=https://api.<customer_istari_fqdn>, ISTARI_DIGITAL_IDENTITY_SERVICE_REGISTRY_PUBLIC_KEY_B64=<public key from step 2>, ISTARI_DIGITAL_IDENTITY_SERVICE_FRONTEND_REDIRECT_URIS=https://<customer_istari_fqdn>, ISTARI_DIGITAL_IDENTITY_SERVICE_MCP_REDIRECT_URIS=https://mcp.<customer_istari_fqdn>/auth/callback
RegistryISTARI_DIGITAL_API_URL=https://api.<customer_istari_fqdn>, ISTARI_DIGITAL_IDENTITY_SERVICE_ENABLED="true", ISTARI_DIGITAL_IDENTITY_SERVICE_CLIENT_CREDENTIALS=<registry_secret from step 1>
FrontendVITE_ISTARI_DIGITAL_API_URL=https://api.<customer_istari_fqdn>, VITE_ISTARI_DIGITAL_IDENTITY_SERVICE_ENABLED="true"
MCP (if deployed)ISTARI_DIGITAL_API_URL=https://api.<customer_istari_fqdn>, ISTARI_DIGITAL_IDENTITY_SERVICE_ENABLED="true", ISTARI_DIGITAL_IDENTITY_SERVICE_CLIENT_SECRET=<a generated value, e.g. openssl rand -hex 32>

Rotating the registry's key​

Generate a new credential, replace the two values, and restart the Identity Service and the registry together. The Identity Service replaces the registry client's key when it restarts, so the registry's requests to it fail until the registry restarts with the new credential.