Skip to main content
Version: 2026.09

Identity Service

The Identity Service is the Istari Digital platform's OpenID Connect (OIDC) and OAuth 2.0 authorization server. It signs people in through an upstream identity provider, then issues its own signed platform tokens, which the registry, frontend, MCP service, CLI and SDK use. Platform services trust only the Identity Service's tokens and never see the upstream identity provider's. By default the upstream identity provider is the Zitadel that your Istari installation deploys and manages; an installation can move to Keycloak or Microsoft Entra ID instead.

It provides:

  • Standard browser sign-in through your identity provider, using the authorization code flow with PKCE; refresh within a session limit you configure; and sign-out that also ends the identity provider session.
  • Key-based authentication for people, SDK clients, agents and platform services (private key JWT, RFC 7523), in place of Personal Access Tokens (PATs).
  • Standard OIDC endpoints for clients and services: discovery, public signing keys (JWKS) and token introspection (RFC 7662).
  • Stable identities it owns for every person, agent and platform service, so accounts, keys, tenant memberships and roles stay the same if you change identity provider.
  • Tenants and administrator roles, managed in the Identity Service and carried in every token it issues.
  • An audit trail of sign-ins, token issuance and administrative changes, which it can forward to your SIEM.

See Developer Settings — Keys, Managing Agents, and the CLI key commands for what users can do once it is enabled.

Installing it is covered elsewhere: enabling the Identity Service in a standard installation is Scenario 10 of the Istari Platform Installation (secret, Helm values, upgrade), with the API Gateway as its routing prerequisite and Clients & Tenants as its follow-up. This page is the reference that supports those: how the service fits into the platform, registering it in Zitadel, generating its keys, and the full configuration surface. For the identity providers it supports, and moving an installation from Zitadel to Keycloak or Microsoft Entra ID, see Identity Providers.

Architecture​

Browsers and agents authenticate against the Identity Service; the Identity Service authenticates people against the upstream identity provider and issues its own signed tokens. Downstream platform services verify those tokens with the Identity Service's public keys or its introspection endpoint — they never see the identity provider's tokens.

The Identity Service needs no public exposure of its own: the API Gateway fronts it at the /identity path prefix, and browsers, agents and the registry service all reach it through https://api.<customer_istari_fqdn>/identity.

It stores its state in a dedicated PostgreSQL database that you create (any name you like; its connection string becomes ISTARI_DIGITAL_IDENTITY_SERVICE_DATABASE_URL). Schema migrations run automatically as a Helm hook — no manual database setup beyond creating the empty database and a role with read/write and DDL rights.

Registering with the Upstream Identity Provider​

The Identity Service is itself an OIDC client of the upstream identity provider and needs an application registration there. The steps below are for the default, the Zitadel your Istari installation manages. For Keycloak, see Switching from Zitadel.

If you installed Zitadel using the Zitadel Configurator 1.8.2 or later, this section is already done, apart from the management key on versions before 1.10.0. The Configurator creates:

  • the Identity Service OIDC application (web, authorization-code, private-key JWT) with its redirect URI, and a JSON application key
  • from Configurator 1.10.0, the Zitadel management key. The chart reads Zitadel with it to create tenants for your Zitadel organizations.

It delivers them in a Kubernetes secret named zitadel-identity-service-env. The Helm values that enable the Identity Service mount that secret.

The chart creates tenants only for organizations that the management key's service user is a member of. Which user that is depends on the Configurator version:

  • 1.11.0 or later: the registry service's Zitadel service user, RegistryServiceMachineUser. The import covers the default organization and each organization that Setting up a new organization makes this user the owner of, if that happens before the import is done.
  • 1.10.x: a service user that is a member of the Configurator's default organization only. Only that organization gets a tenant from the import; create the others by hand.
  • 1.8.2 to 1.9.x: the Configurator delivers no management key, and the install or upgrade fails without one. Create the key by hand (step 4), add only that key to the istari-identity secret, and keep identity.extraEnvSecrets in the Helm values. Or turn the import off.
warning

When installing the Configurator, set configurator.identity_service_base_url to the gateway-prefixed URL: https://api.<customer_istari_fqdn>/identity. That value becomes both the registered redirect URI (<base>/callback) and the BASE_URL the secret delivers — and Zitadel validates the redirect URI on every login, so a dedicated-hostname value there registers a callback that will not match what the Identity Service presents behind the gateway.

Skip to Generate Keys.

Manual registration​

Without the Configurator, or with one older than 1.8.2, register the application yourself:

  1. In the Zitadel console, open the Istari project and create a new application:
    • Type: Web
    • Authentication method: Private Key JWT (recommended; Basic with a client secret is also supported — see OIDC_CLIENT_AUTH_METHOD)
    • Grant type: Authorization Code
    • Redirect URI: https://api.<customer_istari_fqdn>/identity/callback
  2. Record the generated Client ID — this becomes ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_CLIENT_ID.
  3. On the application's Keys tab, add a new key of type JSON and download the key file; base64-encode it verbatim — this becomes ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_PRIVATE_KEY:
base64 < identity-service-app-key.json | tr -d '\n'
  1. Create the Zitadel management key. The chart reads Zitadel with it to create your tenants, and the install or upgrade fails without it unless you turn the import off.

    1. Create a machine user in your Zitadel organization.
    2. Before the install or upgrade that first runs the import, make the machine user a member of every organization that should get a tenant, including its own, with the read-only ORG_OWNER_VIEWER role. The chart creates tenants only for those organizations. A read-only role is enough, because the key is used only to read organizations, people and role grants.
    3. Create a JSON key for the machine user and base64-encode the file, as you did for the application key in step 3. It becomes ISTARI_DIGITAL_IDENTITY_SERVICE_ZITADEL_MANAGER_KEY.

    If you turn the import off and skip this step, administrators cannot manage keys for a person the Identity Service has no record of, such as someone who has never signed in.

Without the Configurator, add these values to the istari-identity secret (Identity Service Secret):

KeyValue
ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_CLIENT_IDThe Client ID from step 2
ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_PRIVATE_KEYThe encoded key from step 3
ISTARI_DIGITAL_IDENTITY_SERVICE_ZITADEL_MANAGER_KEYThe encoded key from step 4, unless you turn the import off
ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_ISSUERhttps://zitadel.<customer_istari_fqdn>
ISTARI_DIGITAL_IDENTITY_SERVICE_BASE_URLhttps://api.<customer_istari_fqdn>/identity

Then omit identity.extraEnvSecrets from the Helm values that enable the Identity Service.

Finding your Zitadel organization ID​

Several steps need the organization ID — a long numeric identifier (typically 18 digits), not a name and not a project resource ID.

  • In the Zitadel console, switch to the instance view and open Organizations: the list shows each organization's Id column — that's the value.
  • Definitive check via API, using any PAT or service-account token for a user in that organization:
curl -s -H "Authorization: Bearer <token>" https://zitadel.<customer_istari_fqdn>/management/v1/orgs/me

The id field of the response is the organization ID. It becomes ISTARI_DIGITAL_IDENTITY_SERVICE_JWT_DEFAULT_TENANT_ID, and names an imported tenant to commands such as ensure-platform-admins. Tenants you create yourself need no organization.

Generate Keys​

You generate two values yourself; neither the upstream identity provider nor the chart's provisioner supplies them:

  • the signing key (required), which signs every token the Identity Service issues and backs its public signing keys (JWKS);
  • the token encryption key (strongly recommended), which encrypts the short-lived token material the Identity Service stores in its database: the upstream identity provider's tokens for signed-in people, and in-flight sign-in state.

Run this from the machine you use for kubectl. It needs only kubectl and the standard base64 and head commands: the signing key is generated in your cluster on the Identity Service image, and the encryption key from your machine's random source.

SIGNING_KEY=$(set -o pipefail; kubectl run gen-signing-key --quiet --rm -i --restart=Never \
--image=istaridigital.jfrog.io/customer-docker/identity-service:<tag> \
--overrides='{"spec":{"imagePullSecrets":[{"name":"docker-pull-secret"}]}}' \
--command -- /gen-signing-key -out /dev/stdout | grep '^{' | base64 | tr -d '\n')
TOKEN_ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
test -n "$SIGNING_KEY" && test -n "$TOKEN_ENCRYPTION_KEY" && echo "Both keys generated" \
|| echo "Key generation failed: check the kubectl output above" >&2

Replace <tag> with the Identity Service image tag you run (the chart's default is 2.1.0). If the command seems to hang, check kubectl get pods for ImagePullBackOff: the pod needs the image pull secret, which --overrides attaches.

Alternative: generate both keys locally with openssl

On a machine with openssl and jq, this makes the same two values without running a pod:

SIGNING_KEY=$(jq -n --arg id "identity-$(date +%Y%m%d)" \
--arg key "$(openssl ecparam -name secp384r1 -genkey -noout | openssl pkcs8 -topk8 -nocrypt)" \
'{keyId: $id, key: $key}' | base64 | tr -d '\n')
TOKEN_ENCRYPTION_KEY=$(openssl rand -base64 32)
test -n "$SIGNING_KEY" && test -n "$TOKEN_ENCRYPTION_KEY" && echo "Both keys generated" \
|| echo "Key generation failed: check the output above" >&2

Put $SIGNING_KEY in the istari-identity secret as ISTARI_DIGITAL_IDENTITY_SERVICE_SIGNING_KEY, and $TOKEN_ENCRYPTION_KEY as ISTARI_DIGITAL_IDENTITY_SERVICE_TOKEN_ENCRYPTION_KEY (Identity Service Secret). Both are secrets: nothing is written to disk, so close the shell when you are done.

warning

Always set the token encryption key in production. Without it the service still starts, with a warning, but it does not keep the upstream identity provider's tokens and stores in-flight sign-in state unencrypted. A malformed value of either key prevents startup.

Key formats, bringing your own signing key, and rotating the encryption key

Signing key. The value is the base64 of a JSON object, {"keyId": "<identifier>", "key": "<private key PEM>"}. The command above makes an ECDSA P-384 key (CNSA 2.0 compliant). A key you bring yourself must be ECDSA P-384 or RSA of at least 3072 bits, in a PKCS#8, SEC1 or PKCS#1 PEM; anything weaker fails validation at startup. The keyId becomes the kid published in the JWKS.

Token encryption key. Each key must be the base64 of exactly 32 bytes, which is what both commands above produce; any other length prevents startup. The variable accepts a comma-separated list: the first key encrypts new data, and every listed key is tried for decryption. To rotate, put a new key first and keep the old ones listed; stored rows are short-lived, so remove old keys after a few days. If the key is lost, encrypted rows are treated as absent and the affected people sign in again; nothing is unrecoverable.

Client credentials​

The registry, frontend and MCP service sign in to the Identity Service as its clients. With the chart's provisioner (provisioner.enabled: true, as in Scenario 10), a Job generates their credentials and the chart sets the three startup settings for you: REGISTRY_PUBLIC_KEY_B64, FRONTEND_REDIRECT_URIS and MCP_REDIRECT_URIS. Without it, the Identity Service does not start until you set them; see Supplying the Credentials Yourself.

Configuration Reference​

The settings the service reads, for tuning beyond the defaults of the Helm install that enables the Identity Service. All variables are prefixed with ISTARI_DIGITAL_IDENTITY_SERVICE_ (omitted below). The tables list the settings of Identity Service 2.1.0, the chart's default image; a setting added after v1.2.2 names the version that added it at the end of its row.

Required​

The service fails to start if any of these is missing. OIDC_PRIVATE_KEY is the exception: it is required only under the default OIDC_CLIENT_AUTH_METHOD, private_key_jwt; a client-secret method needs OIDC_CLIENT_SECRET instead:

VariableDescription
OIDC_ISSUERThe upstream identity provider's issuer URL, no trailing slash — for the default Zitadel, https://zitadel.<customer_istari_fqdn>
OIDC_CLIENT_IDClient ID of the Identity Service's application at the upstream identity provider
OIDC_PRIVATE_KEYBase64-encoded application key JSON from the upstream identity provider (for the default Zitadel, the application's JSON key). Required under the default OIDC_CLIENT_AUTH_METHOD, private_key_jwt; a client-secret method uses OIDC_CLIENT_SECRET instead
BASE_URLPublic base URL — https://api.<customer_istari_fqdn>/identity; the redirect URI registered at the upstream identity provider is <BASE_URL>/callback. May be omitted when ISTARI_DIGITAL_API_URL is set (it then derives to <api>/identity)
SIGNING_KEYBase64-encoded signing key JSON
DATABASE_URLPostgreSQL connection string for the service's dedicated database
CORS_ALLOWED_ORIGINSComma-separated allowed browser origins — the platform frontend URL. (CORS_ALLOW_ALL=true satisfies this for non-production testing only)
REGISTRY_PUBLIC_KEY_B64Base64 of the registry's public key (PEM); registers the registry under the client ID registry. Supplied by the chart's provisioner when enabled
FRONTEND_REDIRECT_URISComma-separated, exact-match redirect allowlist; registers the frontend under the client ID frontend. Derived from common.mainFqdn by the chart when the provisioner is enabled
MCP_REDIRECT_URISComma-separated, exact-match redirect allowlist; registers the MCP service under the client ID mcp, and is required even when the MCP service is not deployed. Derived from common.mainFqdn by the chart when the provisioner is enabled

Required When the Chart's Zitadel Import Runs​

The platform chart's Zitadel import reads Zitadel with the Identity Service's Zitadel management key, the variable in the table below. The import creates a tenant for each Zitadel organization the key's machine user is a member of, and pre-registers each organization's people. The Identity Service itself starts without the key, but the import fails without it, and so does the install or upgrade that runs it.

warning
Supply the key or skip the import (istari-platform chart 6.2.0 and later)

When Zitadel is your identity provider, every install and upgrade with chart 6.2.0 or later runs the import. Supply ZITADEL_MANAGER_KEY, or skip the import:

  • Zitadel Configurator 1.10.0 or later: the Configurator supplies the key in its zitadel-identity-service-env secret, which the Helm install mounts for you.
  • Manual registration: create the key in the machine-user step of Manual registration and add it to the istari-identity secret.
  • To skip the import: set identity.idpMigration.enabled: false in the istari-platform chart values. The import then creates no tenants; create them as in Tenants.
VariableDefaultBehavior
ZITADEL_MANAGER_KEYunsetBase64-encoded JSON key of a Zitadel machine user ({"type", "keyId", "key", "userId"}). Without it, administrators cannot manage keys for another person the Identity Service has no record of yet
VariableDefaultBehavior
TOKEN_ENCRYPTION_KEYunsetAt-rest encryption of stored token material. Unset: warning + degraded; malformed: fatal
ACCESS_TOKEN_TTL15mAccess-token lifetime (Go duration). Must be longer than the 5-minute authorization-code lifetime. A shorter or invalid value is fatal at startup. Added in v2.0.0
OIDC_SCOPESopenid profile email offline_accessZitadel deployments need urn:zitadel:iam:org:project:id:zitadel:aud urn:zitadel:iam:user:resourceowner appended
JWT_DEFAULT_TENANT_IDunsetZitadel organization ID: fallback tenant for instance-level admins
ADMIN_ROLE_KEYcustomer_adminThe Zitadel role the import carries over as the Identity Service's tenant administrator role. Only the import reads it: the service never asks Zitadel for a role at sign-in or on a request
ENROLLMENT_POLICYopenWho may sign in for the first time. open: anyone Zitadel authenticates whose organization maps to a tenant. pre-registered-only: only people the Identity Service already knows, whether an administrator added them or an import, such as the chart's Zitadel import, brought them in. Everyone else is refused. Any other value is fatal at startup. Added in v1.3.0
TENANT_DEACTIVATION_REVOCATION_CASCADEtrueControls how far revocation spreads when a tenant is suspended (deactivated) or a membership is revoked. Revoking a membership always revokes that tenant's role grants. With either value, people and agents left with no active tenant are suspended. true: suspending a tenant revokes its role grants and memberships. People and agents left with no active tenant also lose every other role grant and membership they hold, including the Platform Administrator role. false: neither of those revocations happens. Must be true or false. Added in v2.0.0
ENFORCE_CLIENT_REGISTRATIONtrue from 2.1.0; false before/oauth2/authorize and /api/v2/oauth2/authorize refuse unregistered client IDs and redirect URIs outside a client's allowlist. "false": they are only logged. Scenario 10 sets "true", which also turns it on for earlier versions
ISTARI_DIGITAL_API_URLunsetAPI Gateway base URL (no prefix). When set, BASE_URL and JWT_ISSUER derive to <api>/identity — the recommended configuration
JWT_ISSUERderivediss claim of issued tokens; must match the public URL
JWT_AUDIENCEistari-data-platformaud claim of issued tokens
OIDC_CLIENT_AUTH_METHODprivate_key_jwtHow the service authenticates to the upstream identity provider's token endpoint: private_key_jwt (requires OIDC_PRIVATE_KEY) or client_secret_basic/client_secret_post (require OIDC_CLIENT_SECRET). A method/credential mismatch is fatal at startup
OIDC_CLIENT_SECRETunsetClient secret for the client_secret_* auth methods
OIDC_SUBJECT_CLAIMsubID-token claim used as the stable user identifier
REFRESH_TOKEN_TTL720h (30 days)Refresh-token lifetime (Go duration). Must be positive
MAX_SESSION_DURATION720h (30 days)Session ceiling; refresh tokens never outlive it. Must be positive

Advanced​

VariableDefaultBehavior
HOST / PORT0.0.0.0 / 8000Listen address
IDP_PROVIDERzitadelWhich identity provider the service uses: zitadel, keycloak or entra (entra needs istari-platform 6.5.0 or later). See Identity Providers. An unknown value prevents startup
DISCOVERY_API_VERSION2Which token endpoints OIDC discovery advertises: 2 the current ones, 1 the earlier ones, for clients that still need the earlier token format. Any other value prevents startup, and so does 1 under Entra
OIDC_AUTHORIZATION_ENDPOINT, OIDC_TOKEN_ENDPOINT, OIDC_JWKS_URI, OIDC_USERINFO_ENDPOINT, OIDC_END_SESSION_ENDPOINTunsetExplicit IdP endpoint overrides for providers with broken or absent OIDC discovery; each set value wins over the discovered one. Normally leave unset
OIDC_ASSERTION_AUDIENCEunsetOverrides the aud of the service's own client assertions to the upstream identity provider, for providers with strict conventions. Zitadel and Keycloak accept the default
OIDC_API_BASE_URLOIDC_ISSUERBase URL for Zitadel's HTTP APIs. Set it when the service must reach them at a different address from the issuer that browsers use, such as an in-cluster hostname. Requests to this address still carry the issuer's host in their Host header, so Zitadel picks the right instance. Must be an absolute http or https URL; any other value is fatal at startup. Added in v2.0.0
ADDITIONAL_ASSERTION_AUDIENCESunsetComma-separated extra base URLs accepted as the aud of RFC 7523 client assertions, besides the canonical JWT_ISSUER (always accepted). Absolute lowercase-scheme http(s) URLs, matched exactly; a malformed entry prevents startup. The Helm chart sets the gateway-prefixed audience automatically
ALLOW_WILDCARD_REDIRECTSfalseNon-production only. Lets a registered redirect-allowlist entry carry a single * inside its https hostname (one DNS label, never a dot)
AUDIT_SYSLOG_HOST, AUDIT_SYSLOG_PORT, AUDIT_FORMAT, AUDIT_FALLBACK_PATH, AUDIT_TLS_SKIP_VERIFYunsetForward audit events to a SIEM over TLS syslog; local file fallback; TLS verify skip is for testing only
OTEL_ENABLED / OTEL_SERVICE_NAMEfalse / identity-serviceOpenTelemetry traces over OTLP/HTTP; standard OTEL_* exporter variables are honored

Verification​

curl -fsS https://api.<customer_istari_fqdn>/identity/health/readiness
curl -fsS https://api.<customer_istari_fqdn>/identity/.well-known/openid-configuration
curl -fsS https://api.<customer_istari_fqdn>/identity/.well-known/jwks.json

All three should return 200, and the JWKS response must list your signing key's ID. The browser login flow is verified end to end by logging in to the platform at https://<customer_istari_fqdn> once Scenario 10 is applied — the frontend drives the full /oauth2/authorize → the upstream identity provider → /callback round trip. Registration-level verification is covered in Clients & Tenants.

Troubleshooting​

The registry answers 502 Bad Gateway and says its Zitadel credential was rejected. The message reads The registry's Zitadel credential was rejected (<detail>); check its management key, and the detail is one of:

  • HTTP 401 or HTTP 403, when the registry fetches its Zitadel token, or looks up or lists Zitadel organizations;
  • HTTP 400 invalid_grant or HTTP 400 invalid_client, from Zitadel's token endpoint.

Zitadel rejected the registry's own Zitadel management key, held in the registry's secret. Check that FILE_SERVICE_ZITADEL_USER_MANAGER_SECRET there:

  • exists;
  • has not expired;
  • belongs to the Zitadel instance the installation points at.

If it fails any of these checks, generate a new key as in Generate FILE_SERVICE_ZITADEL_USER_MANAGER_SECRET and put it in the registry's secret, then restart the registry.

Zitadel Configurator 1.11.0 and later: replace the key in two places

These versions set the Identity Service's ZITADEL_MANAGER_KEY, otherwise a separate key, to this registry key. If you run one of them, put the new key in ZITADEL_MANAGER_KEY too.

The registry answers 502 Bad Gateway and names the Identity Service. The message starts with identity-service and says which call failed:

  • identity-service refused <operation>, where the operation names the call, such as agent provisioning. The Identity Service rejected the registry's own client credentials, or did not allow the registry that call. Check that:
    • the registry's client credential is in place: with the provisioner, its istari-provisioner-registry-credentials secret; without it, ISTARI_DIGITAL_IDENTITY_SERVICE_CLIENT_CREDENTIALS in the registry's secret, as in Supplying the Credentials Yourself;
    • the matching public key is registered. The Identity Service registers it at startup from ISTARI_DIGITAL_IDENTITY_SERVICE_REGISTRY_PUBLIC_KEY_B64; Verify a Registration shows how to check.
  • identity-service could not authenticate the registry during <operation>: the registry could not get a token from the Identity Service. Check the same secret and public key.
  • Any other message that starts with identity-service: the Identity Service answered with an error or an unexpected response, or could not be reached. Its logs from the time of the request show the cause.

A 502 that names neither Zitadel nor the Identity Service has another cause. The registry's logs record the call that failed.