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.
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):
| Endpoint | Purpose | Reached by |
|---|---|---|
GET /oauth2/authorize | Starts the browser login flow | Browsers |
GET /callback | Zitadel redirects here after login | Browsers |
POST /oauth2/token | Token endpoint (login completion, key-based agent auth, refresh) | Browsers, SDK, agents |
GET /logout | RP-initiated logout — ends the platform session and redirects to Zitadel's end-session endpoint so the upstream SSO session ends too | Browsers |
POST /oauth/v2/introspect | Token introspection | Registry service |
GET /.well-known/openid-configuration | OIDC discovery | All clients |
GET /.well-known/jwks.json | Public keys for verifying issued tokens | Registry service, MCP, integrations |
/api/v1/* | User, key, and agent management APIs | Frontend, CLI, SDK |
GET /health/liveness, GET /health/readiness | Health probes | Orchestrator |
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.
With the Zitadel Configurator (recommended)
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.
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:
- 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
- Record the generated Client ID — this becomes
ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_CLIENT_ID. - 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'
- (Optional, enables admin key management) Create a machine user in your Zitadel organization, grant it the read-only
ORG_OWNER_VIEWERrole, create a JSON key for it, and base64-encode the file — it becomesISTARI_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.
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.
Token Encryption Key (strongly recommended)
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.
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:
| Variable | Description |
|---|---|
OIDC_ISSUER | Zitadel issuer URL, no trailing slash — https://zitadel.<customer_istari_fqdn> |
OIDC_CLIENT_ID | Client ID of the Zitadel application |
OIDC_PRIVATE_KEY | Base64-encoded Zitadel application key JSON |
BASE_URL | Public 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_KEY | Base64-encoded signing key JSON |
DATABASE_URL | PostgreSQL connection string for the service's dedicated database |
CORS_ALLOWED_ORIGINS | Comma-separated allowed browser origins — the platform frontend URL. (CORS_ALLOW_ALL=true satisfies this for non-production testing only) |
Recommended and Optional
| Variable | Default | Behavior |
|---|---|---|
TOKEN_ENCRYPTION_KEY | unset | At-rest encryption of stored token material. Unset: warning + degraded; malformed: fatal |
OIDC_SCOPES | openid profile email offline_access | Zitadel deployments need urn:zitadel:iam:org:project:id:zitadel:aud urn:zitadel:iam:user:resourceowner appended |
JWT_DEFAULT_TENANT_ID | unset | Zitadel organization ID: fallback tenant for instance-level admins |
ZITADEL_MANAGER_KEY | unset | Base64 Zitadel machine-user key JSON ({"type", "keyId", "key", "userId"}) for admin key management. Unset: admin key ops degrade to self-service |
ADMIN_ROLE_KEY | customer_admin | Zitadel role key granting cross-principal key management |
AGENT_PROVISIONING_CLIENT_IDS | unset | Client IDs allowed to provision agents (the registry's). Unset: agent creation through the platform fails. Set during client registration |
ENFORCE_CLIENT_REGISTRATION | false | "true": /oauth2/authorize rejects unregistered client IDs and non-allowlisted redirect URIs; off = log-only. Enable after registering the frontend |
ISTARI_DIGITAL_API_URL | unset | API Gateway base URL (no prefix). When set, BASE_URL and JWT_ISSUER derive to <api>/identity — the recommended configuration |
JWT_ISSUER | derived | iss claim of issued tokens; must match the public URL |
JWT_AUDIENCE | istari-data-platform | aud claim of issued tokens |
OIDC_CLIENT_AUTH_METHOD | private_key_jwt | How 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_SECRET | unset | Client secret for the client_secret_* auth methods |
OIDC_SUBJECT_CLAIM | sub | ID-token claim used as the stable user identifier |
REFRESH_TOKEN_TTL | 720h (30 days) | Refresh-token lifetime (Go duration). Must be positive |
MAX_SESSION_DURATION | 720h (30 days) | Session ceiling; refresh tokens never outlive it. Must be positive |
Advanced
| Variable | Default | Behavior |
|---|---|---|
HOST / PORT | 0.0.0.0 / 8000 | Listen address |
IDP_PROVIDER | zitadel | Which 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_ENDPOINT | unset | Explicit IdP endpoint overrides for providers with broken or absent OIDC discovery; each set value wins over the discovered one. Normally leave unset |
OIDC_ASSERTION_AUDIENCE | unset | Overrides the aud of the service's own client assertions to Zitadel, for IdPs with strict conventions. Zitadel accepts the default |
ADDITIONAL_ASSERTION_AUDIENCES | unset | Comma-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_REDIRECTS | false | Non-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_VERIFY | unset | Forward audit events to a SIEM over TLS syslog; local file fallback; TLS verify skip is for testing only |
OTEL_ENABLED / OTEL_SERVICE_NAME | false / identity-service | OpenTelemetry 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.