Skip to main content
Version: 2026.08

Identity Service

The Identity Service is the authentication service for the Istari Digital platform. It sits between your identity provider (Zitadel) and the platform services: users and agents authenticate against it, and it issues short-lived, signed platform tokens that downstream services (the registry, the frontend) verify. Enabling it is what turns on key-based authentication — users, SDK clients, and agents can then authenticate with cryptographic keys instead of Personal Access Tokens (PATs). See Developer Settings — Keys, Managing Agents, and the CLI key commands for what users can do once it is enabled.

info

The Identity Service was previously named the Identity Router. The legacy name still appears in some configuration keys (e.g. FILE_SERVICE_IDENTITY_ROUTER_*, VITE_IDENTITY_ROUTER_*) — these are literal identifiers and are used as-is throughout this guide. Both names refer to the same service.

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 Registering Clients & Tenants as its follow-up. This page is the reference that supports those: what the service exposes, registering it in Zitadel, generating its keys, and the full configuration surface.

Architecture and Endpoints

Browsers and agents authenticate against the Identity Service; the Identity Service authenticates users against Zitadel and issues its own signed JWTs. Downstream platform services verify those JWTs using the Identity Service's public keys (JWKS) or its token introspection endpoint — they never see Zitadel tokens directly.

Paths are relative to the service root; on the public gateway URL they appear under the /identity prefix (e.g. https://api.<customer_istari_fqdn>/identity/oauth2/authorize):

EndpointPurposeReached by
GET /oauth2/authorizeStarts the browser login flowBrowsers
GET /callbackZitadel redirects here after loginBrowsers
POST /oauth2/tokenToken endpoint (login completion, key-based agent auth, refresh)Browsers, SDK, agents
GET /logoutRP-initiated logout — ends the platform session and redirects to Zitadel's end-session endpoint so the upstream SSO session ends tooBrowsers
POST /oauth/v2/introspectToken introspectionRegistry service
GET /.well-known/openid-configurationOIDC discoveryAll clients
GET /.well-known/jwks.jsonPublic keys for verifying issued tokensRegistry service, MCP, integrations
/api/v1/*User, key, and agent management APIsFrontend, CLI, SDK
GET /health/liveness, GET /health/readinessHealth probesOrchestrator

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.

Zitadel Registration

The Identity Service is itself an OIDC client of Zitadel and needs an application registration there.

If you installed Zitadel using the Zitadel Configurator, this section is already done. The Configurator creates:

  • the Identity Service OIDC application (web, authorization-code, private-key JWT) with its redirect URI, and a JSON application key
  • a dedicated read-only service user (ORG_OWNER_VIEWER) with a JSON key, used for admin key-management checks

and delivers all of it in a Kubernetes secret named zitadel-identity-service-env (OIDC_ISSUER, BASE_URL, OIDC_CLIENT_ID, OIDC_PRIVATE_KEY, ZITADEL_MANAGER_KEY). Scenario 10 mounts that secret via identity.extraEnvSecrets — you never handle these values directly.

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, 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. (Optional, enables admin key management) Create a machine user in your Zitadel organization, grant it the read-only ORG_OWNER_VIEWER role, create a JSON key for it, and base64-encode the file — it becomes ISTARI_DIGITAL_IDENTITY_SERVICE_ZITADEL_MANAGER_KEY. Use a read-only role: the Identity Service only reads role grants with it. Skipping this only means administrators cannot manage other principals' keys (self-service still works).

Add the values from these steps — ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_CLIENT_ID, ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_PRIVATE_KEY, and (if created) ISTARI_DIGITAL_IDENTITY_SERVICE_ZITADEL_MANAGER_KEY — to the istari-identity secret (Identity Service Secret), along with ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_ISSUER (https://zitadel.<customer_istari_fqdn>) and ISTARI_DIGITAL_IDENTITY_SERVICE_BASE_URL (https://api.<customer_istari_fqdn>/identity), and omit identity.extraEnvSecrets from the Scenario 10 values.

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 the -provider-tenant-id used when creating tenants.

Generate Keys

Two secrets are generated by you (rather than coming from Zitadel). Both commands run as one-off pods on the service image — no Docker required.

warning

Replace <tag> in every command below with a real Identity Service image tag (e.g. 1.2.2) — it's easy to miss. If a command appears to hang, check kubectl get pods for ImagePullBackOff: one-off pods need the image pull secret, which the commands below attach via --overrides.

Signing Key (required)

The signing key signs every token the Identity Service issues and backs its public /.well-known/jwks.json endpoint. gen-signing-key produces an ECDSA P-384 key (CNSA 2.0 compliant):

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 '^{' > signing-key.json

base64 < signing-key.json | tr -d '\n'

The base64 output becomes ISTARI_DIGITAL_IDENTITY_SERVICE_SIGNING_KEY. Treat signing-key.json as a secret: load it into your secret store and delete the local file.

Format: the file is a JSON blob with the shape {"keyId": "<identifier>", "key": "<private key PEM>"}, and the environment variable is the base64 of that JSON. If you bring your own key instead, it must satisfy CNSA 2.0: ECDSA P-384 or RSA of at least 3072 bits (PKCS#8, SEC1, and PKCS#1 PEM encodings are all accepted); anything weaker fails validation at startup. The keyId becomes the kid published in the JWKS.

The Identity Service stores short-lived token material in its database (Zitadel tokens for logged-in users, in-flight login state). This key encrypts that material at rest with AES-256-GCM:

openssl rand -base64 32

The output becomes ISTARI_DIGITAL_IDENTITY_SERVICE_TOKEN_ENCRYPTION_KEY.

Format: each key in the value must be the base64 encoding of exactly 32 bytes — which is precisely what openssl rand -base64 32 produces. Any other length is rejected at startup.

Rotation: the variable accepts a comma-separated list. The first key encrypts all new data; every listed key is still tried for decryption. To rotate, put a new key first and keep the old key(s) listed; stored rows are short-lived, so old keys can be removed after a few days. If the key is lost, encrypted rows are treated as absent — affected users log in again; nothing is unrecoverable.

warning

Always set TOKEN_ENCRYPTION_KEY in production. Without it the service still starts (with a warning), but Zitadel-token persistence is disabled and in-flight login state is stored unencrypted. A malformed value prevents startup.

The remaining generated values — the registry's client credentials and the frontend client ID — are covered in Registering Clients, including how to derive the public-only blob the chart's registration hook expects.

Configuration Reference

Everything the service reads, for tuning beyond the Scenario 10 defaults. All variables are prefixed with ISTARI_DIGITAL_IDENTITY_SERVICE_ (omitted below). Current as of Identity Service v1.2.2.

Required

The service fails to start if any of these is missing:

VariableDescription
OIDC_ISSUERZitadel issuer URL, no trailing slash — https://zitadel.<customer_istari_fqdn>
OIDC_CLIENT_IDClient ID of the Zitadel application
OIDC_PRIVATE_KEYBase64-encoded Zitadel application key JSON
BASE_URLPublic base URL — https://api.<customer_istari_fqdn>/identity; the Zitadel redirect URI 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)
VariableDefaultBehavior
TOKEN_ENCRYPTION_KEYunsetAt-rest encryption of stored token material. Unset: warning + degraded; malformed: fatal
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
ZITADEL_MANAGER_KEYunsetBase64 Zitadel machine-user key JSON ({"type", "keyId", "key", "userId"}) for admin key management. Unset: admin key ops degrade to self-service
ADMIN_ROLE_KEYcustomer_adminZitadel role key granting cross-principal key management
AGENT_PROVISIONING_CLIENT_IDSunsetClient IDs allowed to provision agents (the registry's). Unset: agent creation through the platform fails. Set during client registration
ENFORCE_CLIENT_REGISTRATIONfalse"true": /oauth2/authorize rejects unregistered client IDs and non-allowlisted redirect URIs; off = log-only. Enable after registering the frontend
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 Zitadel'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 upstream IdP integration to use. An unknown value is fatal at startup
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 Zitadel, for IdPs with strict conventions. Zitadel accepts the default
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 → Zitadel → /callback round trip. Registration-level verification is covered in Registering Clients & Tenants.

Once browser login is verified, harden the authorize endpoint by setting ISTARI_DIGITAL_IDENTITY_SERVICE_ENFORCE_CLIENT_REGISTRATION: "true" in the istari-identity secret and restarting the Identity Service.