Identity Service: Registering Clients & Tenants
Once the Identity Service is deployed (Scenario 10), the platform services that authenticate against it must be registered with it, and every Zitadel organization you use must have a matching tenant.
In a chart install you do not run registration commands. You generate credential values once, place them in the istari-identity secret, and the chart's hook Jobs register everything on every helm upgrade. This page covers generating those values, tenants, and — for installations not using the chart — the manual equivalents.
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.
Concepts
Registration happens at two independent layers: the Identity Service itself is an OIDC client of Zitadel (Zitadel Registration), and platform services are in turn clients of the Identity Service. The registry holds an ECDSA P-384 key pair and authenticates with signed JWT assertions (RFC 7523); browser-based public clients (the frontend, the MCP service) carry no key — their registration is a client ID plus an exact-match allowlist of redirect URIs.
Two properties hold throughout: the Identity Service stores only public keys (private halves stay with the client and are never sent to it), and every registration is an idempotent upsert (re-running replaces keys or allowlists — which is also how you rotate them).
How Registration Works
The chart turns the contents of the istari-identity secret into registrations, via pre-install/pre-upgrade hook Jobs that run on every install and upgrade (idempotently — safe to re-run forever):
| You put in the secret | Hook Job (Scenario 10 values) | What gets registered |
|---|---|---|
ISTARI_DIGITAL_IDENTITY_SERVICE_REGISTRY_CLIENT (public-only blob) | identity.registryClientRegistration | The registry service as a trusted client — enables token introspection and agent provisioning |
ISTARI_DIGITAL_IDENTITY_SERVICE_FRONTEND_CLIENT_ID + ..._FRONTEND_REDIRECT_URIS | identity.publicClientRegistration (name: frontend) | The frontend as a public client with its redirect allowlist |
| MCP client ID + redirect URIs keys (if deployed) | identity.publicClientRegistration (name: mcp) | The MCP service as a public client |
A missing secret key fails the Helm release at hook time — deliberate, so a client is never silently left unregistered. Your only hands-on work is producing the values, below.
Generate the Client Credentials
One-time (per environment) generation of the values the secret carries. The generator runs as a one-off pod on the service image — no Docker required, no database access needed:
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 a real Identity Service image tag (e.g. 1.2.2). If the pod seems to hang, check kubectl get pods for ImagePullBackOff — a missing pull secret is the usual cause, and --rm -i can otherwise mask it.
Output:
{
"registry_secret": "<base64 credentials blob — contains the PRIVATE key>",
"registry_client_id": "registry-a1b2",
"frontend_client_id": "frontend-c3d4"
}
Derive the public-only blob for the identity secret — never store the private blob there:
# From the registry_secret value above:
echo "<registry_secret>" | base64 -d > registry-blob.json
jq --arg key "$(jq -r .key registry-blob.json | openssl pkey -pubout)" \
'.key = $key' registry-blob.json | base64 | tr -d '\n'
rm registry-blob.json
Distribute the values — after this, the hooks handle everything on the next helm upgrade:
| Value | Goes into |
|---|---|
registry_secret (private blob) | The registry's secret, as FILE_SERVICE_IDENTITY_ROUTER_SECRET |
| The public-only blob (derived above) | The identity secret, as ISTARI_DIGITAL_IDENTITY_SERVICE_REGISTRY_CLIENT |
registry_client_id | The identity secret, as ISTARI_DIGITAL_IDENTITY_SERVICE_AGENT_PROVISIONING_CLIENT_IDS — without it, agent creation through the platform fails |
frontend_client_id | The identity secret as ISTARI_DIGITAL_IDENTITY_SERVICE_FRONTEND_CLIENT_ID, and the frontend's secret as VITE_IDENTITY_ROUTER_CLIENT_ID |
| Frontend redirect allowlist | The identity secret as ISTARI_DIGITAL_IDENTITY_SERVICE_FRONTEND_REDIRECT_URIS — comma-separated, exact-match (no wildcards or prefixes, no trailing-slash forgiveness), for the SPA just https://<customer_istari_fqdn> |
If the MCP service is deployed, add its two keys the same way (client ID of your choosing; redirect URI https://mcp.<customer_istari_fqdn>/auth/callback) and list it under identity.publicClientRegistration.clients.
Tenants
A tenant is the Identity Service's own grouping of principals — every agent and user belongs to one. Tenants map to Zitadel organizations:
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 (
-provider-name zitadel -provider-tenant-id <org id>) — links the tenant to a Zitadel organization ID, so tokens issued to the tenant's principals carry the organization context role lookups need.
Tenants are never created automatically. Agent creation through the platform requires the calling organization's tenant to already exist — a missing mapping fails with tenant_not_provisioned, and is the usual cause of key or agent creation failing for an organization. Tenant creation is also the one registration task with no chart hook (yet): run one create-tenant per Zitadel organization — including your first — as a one-off pod (DATABASE_URL is injected from the identity secret automatically; 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>",
"-provider-name","zitadel",
"-provider-tenant-id","<zitadel_org_id>"],
"env":[{"name":"DATABASE_URL","valueFrom":{"secretKeyRef":{
"name":"istari-identity",
"key":"ISTARI_DIGITAL_IDENTITY_SERVICE_DATABASE_URL"}}}]
}]}
}'
Re-running with the same slug is safe. There is no per-tenant client registration: the registry/frontend registrations above are platform-wide and cover all tenants.
Enforcement
Once every public client is registered and browser login is verified, harden /oauth2/authorize by setting ISTARI_DIGITAL_IDENTITY_SERVICE_ENFORCE_CLIENT_REGISTRATION: "true" in the identity secret and restarting the Identity Service. Off (the default), unregistered clients are only logged; on, they are rejected.
Enable enforcement only after the frontend (and, if deployed, the MCP) registrations have run — enabling it first locks every browser login out. The lockout is fully recoverable and nothing is lost: register the missing client (effective on the next login attempt, no restart needed) or set ENFORCE_CLIENT_REGISTRATION back to "false" and restart the Identity Service.
Manual Registration (non-chart installs)
Installations that don't use the Istari Platform Helm chart run the registration commands themselves. The snippets below are not shell commands — the image is distroless, so each is the command (first token) and args (the rest) to substitute into the one-off pod template shown under Tenants. For example, the registry registration in full:
kubectl run register-client --rm -i --restart=Never \
--image=istaridigital.jfrog.io/customer-docker/identity-service:<tag> \
--overrides='{
"spec":{
"imagePullSecrets":[{"name":"docker-pull-secret"}],
"containers":[{
"name":"register-client",
"image":"istaridigital.jfrog.io/customer-docker/identity-service:<tag>",
"command":["/register-client"],
"args":["-database-url","$(DATABASE_URL)","-secret","<public-only blob>"],
"env":[{"name":"DATABASE_URL","valueFrom":{"secretKeyRef":{
"name":"istari-identity",
"key":"ISTARI_DIGITAL_IDENTITY_SERVICE_DATABASE_URL"}}}]
}]}
}'
The remaining registrations substitute these command/args pairs:
-
Registry service (stores only the public half, so either blob works — public-only preferred):
/register-client -database-url "$(DATABASE_URL)" -secret "<public-only blob>" -
Frontend / MCP (public clients — ID plus exact-match redirect allowlist):
/register-client -database-url "$(DATABASE_URL)" \-public-client-id "<frontend_client_id>" \-redirect-uris "https://<customer_istari_fqdn>"
Non-chart installs also wire the consuming services by hand (the chart renders all of this automatically from identity.clientIntegration.enabled + apiGateway.apiUrl):
| Service | Variables |
|---|---|
| Registry | FILE_SERVICE_FEATURE_FLAGS__IDENTITY_ROUTER_ENABLED=true, FILE_SERVICE_IDENTITY_ROUTER_URL=https://api.<customer_istari_fqdn>/identity, FILE_SERVICE_IDENTITY_ROUTER_SECRET=<registry PRIVATE blob> |
| Frontend | VITE_IDENTITY_ROUTER_ENABLED="true", VITE_IDENTITY_ROUTER_AUTHORITY=https://api.<customer_istari_fqdn>/identity, VITE_IDENTITY_ROUTER_CLIENT_ID=<frontend_client_id> |
| MCP (if deployed) | ISTARI_DIGITAL_IDENTITY_SERVICE_ENABLED="true", ISTARI_DIGITAL_IDENTITY_SERVICE_ISSUER=https://api.<customer_istari_fqdn>/identity, ISTARI_DIGITAL_IDENTITY_SERVICE_CLIENT_ID=<mcp_client_id>, ISTARI_DIGITAL_IDENTITY_SERVICE_CLIENT_SECRET=<any generated value> — the secret is the MCP's own configuration requirement; the Identity Service registers the MCP as a public client (ID + redirect allowlist) and current releases do not verify the secret value |
Restart each service after setting its variables.
Verify a Registration
Easiest — through the platform, no extra tooling:
- Log in at
https://<customer_istari_fqdn>— a successful login proves the frontend's public-client registration and the whole browser flow. - Generate a key from Developer Settings — Keys or create an agent from the admin Agents page — success proves the registry's client registration, the provisioning allowlist, 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 at POST /oauth2/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: user_type, the tenant's slug, and the user or agent identity. See the CLI documentation for installation.