Skip to main content
Version: 2026.07

Identity Service: Install & Configure

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.

This page covers installing and configuring the Identity Service itself. Registering platform services with it is covered in Registering Clients; agents are provisioned through the platform (see Managing Agents).

Architecture and Exposed 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.

The following endpoints must be reachable at the Identity Service's public URL. 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
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

Prerequisites

  • A PostgreSQL database dedicated to the Identity Service (its tables live in a schema named identity_router; no PostgreSQL extensions are required). The database role needs full read/write on that schema; the migration step additionally needs DDL rights (create schema/table/index).
  • Zitadel installed and configured (see Zitadel Install and Zitadel Configuration), with access to the Zitadel console to register an application.
  • A publicly routable API gateway host (https://api.<customer_istari_fqdn>, served by the platform's service router from the Istari Platform Helm chart). The Identity Service itself needs no public exposure of its own — the service router fronts it at the /identity path prefix, and browsers, agents, and the registry service all reach it through https://api.<customer_istari_fqdn>/identity.
  • Access to the container image istaridigital.jfrog.io/customer-docker/identity-service:<tag> (see Docker Pull Secret). The image is distroless (no shell), runs as a non-root user (UID 65532), and listens on port 8000.
  • The ability to run one-off containers from that image — the same image ships the database migration and registration command-line tools.
  • The examples below assume Kubernetes and the Istari Platform Helm chart, but any orchestrator that can run the container with the environment variables described here works.

Step 1 — Register an OIDC Application in Zitadel

The Identity Service authenticates users against Zitadel as an OIDC client, so it needs its own application registration:

  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)
    • 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. The downloaded JSON already has the shape the Identity Service expects ({"type": ..., "keyId": ..., "key": ..., "appId": ..., "clientId": ...}); base64-encode it verbatim — this becomes ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_PRIVATE_KEY:
base64 < identity-service-app-key.json | tr -d '\n'
  1. Record your Zitadel organization ID (Organization → detail view). It becomes ISTARI_DIGITAL_IDENTITY_SERVICE_JWT_DEFAULT_TENANT_ID, the fallback tenant for Zitadel instance-level administrators.

Optional: Service User for Admin Key Management

Administrators managing other principals' keys — other users' keys, and agents' keys — requires the Identity Service to check the caller's customer_admin role in Zitadel. That check needs a Zitadel service user (machine user) with a JSON key:

  1. Create a machine user in your Zitadel organization and grant it a read-only role that can read user grants — ORG_OWNER_VIEWER is recommended.
  2. Create a JSON key for that user and download it.
  3. Base64-encode the key file — it becomes ISTARI_DIGITAL_IDENTITY_SERVICE_ZITADEL_MANAGER_KEY.
warning

Use a read-only role for this service user. The Identity Service only reads role grants with it; a role that can modify grants would give the key more power than it needs.

If you skip this, the Identity Service still runs — administrators simply cannot manage other principals' keys through the platform (self-service key management still works).

Step 2 — Generate the Identity Service's Keys

Two secrets are generated by you (rather than downloaded from Zitadel).

Signing Key (required)

The signing key signs every token the Identity Service issues and backs its public /.well-known/jwks.json endpoint. Generate it with the gen-signing-key tool shipped in the image (it produces an ECDSA P-384 key, compliant with CNSA 2.0), using either invocation:

Kubernetes one-off pod (no Docker required — the tool prints the key JSON to stdout; the grep drops the tool's log lines, and --quiet keeps kubectl's own status messages out of the captured output):

kubectl -n istari run gen-signing-key --quiet --rm -i --restart=Never \
--image=istaridigital.jfrog.io/customer-docker/identity-service:<tag> \
--command -- /gen-signing-key -out /dev/stdout \
| grep '^{' > signing-key.json

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

Plain container (any Docker-compatible runtime):

docker run --rm --user "$(id -u)" -v "$PWD":/keys \
--entrypoint /gen-signing-key \
istaridigital.jfrog.io/customer-docker/identity-service:<tag> \
-out /keys/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.

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, protecting it even if the database or a backup is accessed directly. Generate a 32-byte key:

openssl rand -base64 32

The output becomes ISTARI_DIGITAL_IDENTITY_SERVICE_TOKEN_ENCRYPTION_KEY.

Rotation: the variable accepts a comma-separated list of keys. The first key encrypts all new data; every listed key is still tried for decryption. To rotate, generate a new key and put it first, keeping the old key(s) in the list; stored rows are short-lived, so old keys can be removed after a few days.

If the key is lost, encrypted rows are simply treated as absent — affected users log in again and no data is unrecoverable.

warning

Always set TOKEN_ENCRYPTION_KEY in production. Without it the service still starts (with a loud warning), but Zitadel token persistence is disabled — user-management operations require users to re-login — and in-flight login state is stored unencrypted. A key that is present but malformed prevents startup.

Configuration Reference

All variables are prefixed with ISTARI_DIGITAL_IDENTITY_SERVICE_ (omitted from the tables below for readability).

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 application from Step 1
OIDC_PRIVATE_KEYBase64-encoded Zitadel application key JSON from Step 1
BASE_URLPublic base URL of the Identity Service — https://api.<customer_istari_fqdn>/identity. The Zitadel redirect URI is <BASE_URL>/callback. May be omitted when ISTARI_DIGITAL_API_URL is set (see below) — it then defaults to <ISTARI_DIGITAL_API_URL>/identity
SIGNING_KEYBase64-encoded signing key JSON from Step 2
DATABASE_URLPostgreSQL connection string — postgresql://<user>:<password>@<host>:5432/<database>
CORS_ALLOWED_ORIGINSComma-separated list of allowed browser origins — set to the platform frontend URL, e.g. https://<customer_istari_fqdn>
warning

CORS_ALLOW_ALL=true satisfies the CORS requirement instead of CORS_ALLOWED_ORIGINS, but reflects any origin and must not be used in production.

VariableDefaultBehavior
TOKEN_ENCRYPTION_KEYunsetEncrypts stored token material at rest (see Step 2). Unset: service starts with a warning, Zitadel-token persistence disabled, login state stored unencrypted. Malformed: startup fails.
OIDC_SCOPESopenid profile email offline_accessScopes requested from Zitadel. Set this — Zitadel deployments need the additional scopes urn:zitadel:iam:org:project:id:zitadel:aud urn:zitadel:iam:user:resourceowner (space-separated, appended to the defaults).
JWT_DEFAULT_TENANT_IDunsetZitadel organization ID used as the fallback tenant for instance-level admins (see Step 1).
ZITADEL_MANAGER_KEYunsetBase64-encoded Zitadel service-user key JSON (see Step 1). Unset: admin key management degrades to self-service only.
ADMIN_ROLE_KEYcustomer_adminZitadel role key that grants cross-principal key management.
AGENT_PROVISIONING_CLIENT_IDSunsetComma-separated client IDs allowed to provision agents (the registry service's client ID). Unset: agent creation through the platform fails. Set during client registration.
ENFORCE_CLIENT_REGISTRATIONfalse"true": /oauth2/authorize rejects unregistered client IDs and redirect URIs outside the registered allowlist; unset/false: violations are only logged. Enable after registering the frontend as a public client.
ISTARI_DIGITAL_API_URLunsetThe platform's API gateway base URL, https://api.<customer_istari_fqdn> (no prefix). When set, BASE_URL and JWT_ISSUER each default to <ISTARI_DIGITAL_API_URL>/identity unless set explicitly — the recommended configuration for router-fronted deployments.
JWT_ISSUERderivediss claim of issued tokens. Defaults to <ISTARI_DIGITAL_API_URL>/identity when that is set; otherwise set it to the same value as BASE_URL.
JWT_AUDIENCEistari-data-platformaud claim of issued tokens.
OIDC_CLIENT_SECRETunsetAlternative to OIDC_PRIVATE_KEY if the Zitadel app uses Basic authentication.
OIDC_SUBJECT_CLAIMsubID-token claim used as the stable user identifier.
REFRESH_TOKEN_TTL720h (30 days)Refresh-token lifetime (Go duration, e.g. 168h). Must be positive.
MAX_SESSION_DURATION720h (30 days)Maximum session lifetime; refresh tokens never outlive the session. Must be positive.

Advanced

VariableDefaultBehavior
HOST / PORT0.0.0.0 / 8000Listen address.
AUDIT_SYSLOG_HOST, AUDIT_SYSLOG_PORTunsetForward audit events to a SIEM over TLS syslog.
AUDIT_FORMATunsetAudit event format.
AUDIT_FALLBACK_PATHunsetLocal file fallback for audit events when syslog is unreachable.
AUDIT_TLS_SKIP_VERIFYfalseSkip TLS verification for the syslog connection (testing only).
OTEL_ENABLEDfalsetrue exports OpenTelemetry traces over OTLP/HTTP; the standard OTEL_EXPORTER_OTLP_ENDPOINT and related OTEL_* variables are honored.
OTEL_SERVICE_NAMEidentity-serviceOpenTelemetry service.name.

Step 3 — Create the Kubernetes Secret

Create a secret holding the environment variables. The Istari Platform Helm chart mounts every key of this secret as an environment variable on the Identity Service container.

info

Make sure to replace <customer_istari_fqdn> with the actual domain used for the Istari Digital platform, and the <...> placeholders with the values gathered in Steps 1–2.

istari-identity-service-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: istari-identity-service
stringData:
ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_ISSUER: "https://zitadel.<customer_istari_fqdn>"
ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_CLIENT_ID: "<zitadel_app_client_id>"
ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_PRIVATE_KEY: "<base64_zitadel_app_key_json>"
ISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_SCOPES: "openid profile email offline_access urn:zitadel:iam:org:project:id:zitadel:aud urn:zitadel:iam:user:resourceowner"
# BASE_URL and JWT_ISSUER derive from this as <ISTARI_DIGITAL_API_URL>/identity;
# set them explicitly only to override (e.g. a dedicated identity hostname).
ISTARI_DIGITAL_API_URL: "https://api.<customer_istari_fqdn>"
ISTARI_DIGITAL_IDENTITY_SERVICE_JWT_DEFAULT_TENANT_ID: "<zitadel_org_id>"
ISTARI_DIGITAL_IDENTITY_SERVICE_SIGNING_KEY: "<base64_signing_key_json>"
ISTARI_DIGITAL_IDENTITY_SERVICE_TOKEN_ENCRYPTION_KEY: "<output_of_openssl_rand_base64_32>"
ISTARI_DIGITAL_IDENTITY_SERVICE_DATABASE_URL: "postgresql://<db_user>:<db_password>@<db_host>:5432/<db_name>"
ISTARI_DIGITAL_IDENTITY_SERVICE_CORS_ALLOWED_ORIGINS: "https://<customer_istari_fqdn>"
# Optional — admin key management (see Step 1):
# ISTARI_DIGITAL_IDENTITY_SERVICE_ZITADEL_MANAGER_KEY: "<base64_zitadel_service_user_key_json>"
kubectl apply -f istari-identity-service-secret.yaml -n istari

Step 4 — Deploy with the Istari Platform Helm Chart

Enable the Identity Service in your Istari Platform Helm values:

values.yaml (excerpt)
identity:
enabled: true
registry: "istaridigital.jfrog.io/customer-docker"
image: "identity-service"
tag: "<tag>"
# Name of the secret created in Step 3
secretName: "istari-identity-service"
# Run database migrations as a pre-install/pre-upgrade Job.
# Recommended; required if you use the chart's registration hooks.
migrations:
runAsJob: true

The chart runs database migrations automatically (as an init container by default, or as a pre-install/pre-upgrade Job with migrations.runAsJob: true), wires the secret to the container, and creates the Service (port 80 → container port 8000) with health probes on /health/liveness and /health/readiness.

info

Not using the Istari Helm chart? The Identity Service is a single stateless container: run istaridigital.jfrog.io/customer-docker/identity-service:<tag> with the environment variables above, apply database migrations before first start and on each upgrade (see below), and route HTTPS traffic for the public URL to container port 8000. The service listens at the root path — when serving it under the /identity prefix, your proxy must strip the prefix before forwarding (the platform's service router does this automatically). Any orchestrator works.

Migrations are applied with the /migrate binary from the same image, which reads ISTARI_DIGITAL_IDENTITY_SERVICE_DATABASE_URL:

docker run --rm \
-e ISTARI_DIGITAL_IDENTITY_SERVICE_DATABASE_URL="postgresql://<db_user>:<db_password>@<db_host>:5432/<db_name>" \
--entrypoint /migrate \
istaridigital.jfrog.io/customer-docker/identity-service:<tag>

No exposure step is needed: the platform's service router (deployed by the same chart) serves the Identity Service on the API gateway host — it matches the /identity path prefix on https://api.<customer_istari_fqdn>, strips the prefix, and forwards to port 8000. The effective BASE_URL/JWT_ISSUER must match that public URL exactly (which the ISTARI_DIGITAL_API_URL derivation from Step 3 guarantees), and the Zitadel redirect URI from Step 1 must be <BASE_URL>/callback. Note the registry service reaches the Identity Service through this public URL, not through cluster-internal addressing.

Step 5 — Verify

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 can only be verified end to end once the frontend is switched over in Step 6: log in to the platform at https://<customer_istari_fqdn> — the frontend drives the full /oauth2/authorize → Zitadel → /callback round trip.

Step 6 — Enable the Identity Service in Platform Components

Once the Identity Service is running and its clients are registered (see Registering Clients — the registry's credentials referenced below are created there), switch the platform components over to it. Deploying the Identity Service does nothing by itself: each consuming service has its own enable flag and must be switched over — and restarted — explicitly.

Registry service — add to the registry's secret:

VariableValue
FILE_SERVICE_FEATURE_FLAGS__IDENTITY_ROUTER_ENABLEDtrue
FILE_SERVICE_IDENTITY_ROUTER_URLhttps://api.<customer_istari_fqdn>/identity
FILE_SERVICE_IDENTITY_ROUTER_SECRETThe registry's client credentials blob (base64 {clientId, keyId, key})

Frontend — add to the frontend secret (see Istari Platform Installation):

VariableValue
VITE_IDENTITY_ROUTER_ENABLED"true"
VITE_IDENTITY_ROUTER_AUTHORITYhttps://api.<customer_istari_fqdn>/identity
VITE_IDENTITY_ROUTER_CLIENT_IDThe frontend's client ID (generated during client registration)

Secure Connection Service — if deployed (see Secure Connection Service), add to the SCS secret:

VariableValue
ISTARI_DIGITAL_IDENTITY_ROUTER_ENABLED"true"
ISTARI_DIGITAL_IDENTITY_ROUTER_URLhttps://api.<customer_istari_fqdn>/identity
ISTARI_DIGITAL_IDENTITY_ROUTER_AGENT_KEYThe SCS's agent credentials blob (base64 {clientId, keyId, key}, private key included). Its public half is registered automatically by the Helm chart's identity.agentRegistration hook

MCP service — if deployed, add to the MCP secret:

VariableValue
ISTARI_DIGITAL_IDENTITY_SERVICE_ENABLED"true"
ISTARI_DIGITAL_IDENTITY_SERVICE_ISSUERhttps://api.<customer_istari_fqdn>/identity
ISTARI_DIGITAL_IDENTITY_SERVICE_CLIENT_IDA client ID for the MCP service
ISTARI_DIGITAL_IDENTITY_SERVICE_CLIENT_SECRETA generated secret for that client
note

The MCP integration is version-gated: current Identity Service releases (through v1.2.1) do not yet verify client secrets, so leave the MCP switched over only if your platform release notes list Identity Service support for it. When switching it over, also register the MCP's client ID so enforcement does not block its logins.

Restart each service after setting its variables. Users can now generate and manage keys — see Developer Settings — Keys and Managing Agents.

Finally, once browser login through the frontend is verified end to end, harden the authorize endpoint by setting ISTARI_DIGITAL_IDENTITY_SERVICE_ENFORCE_CLIENT_REGISTRATION: "true" in the Identity Service's secret and restarting it (see Registering the Frontend as a Public Client).