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.
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):
| 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 |
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 |
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/identitypath prefix, and browsers, agents, and the registry service all reach it throughhttps://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:
- 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
- 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. The downloaded JSON already has the shape the Identity Service expects (
{"type": ..., "keyId": ..., "key": ..., "appId": ..., "clientId": ...}); base64-encode it verbatim — this becomesISTARI_DIGITAL_IDENTITY_SERVICE_OIDC_PRIVATE_KEY:
base64 < identity-service-app-key.json | tr -d '\n'
- 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:
- Create a machine user in your Zitadel organization and grant it a read-only role that can read user grants —
ORG_OWNER_VIEWERis recommended. - Create a JSON key for that user and download it.
- Base64-encode the key file — it becomes
ISTARI_DIGITAL_IDENTITY_SERVICE_ZITADEL_MANAGER_KEY.
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.
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, 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.
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:
| Variable | Description |
|---|---|
OIDC_ISSUER | Zitadel issuer URL, no trailing slash — https://zitadel.<customer_istari_fqdn> |
OIDC_CLIENT_ID | Client ID of the application from Step 1 |
OIDC_PRIVATE_KEY | Base64-encoded Zitadel application key JSON from Step 1 |
BASE_URL | Public 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_KEY | Base64-encoded signing key JSON from Step 2 |
DATABASE_URL | PostgreSQL connection string — postgresql://<user>:<password>@<host>:5432/<database> |
CORS_ALLOWED_ORIGINS | Comma-separated list of allowed browser origins — set to the platform frontend URL, e.g. https://<customer_istari_fqdn> |
CORS_ALLOW_ALL=true satisfies the CORS requirement instead of CORS_ALLOWED_ORIGINS, but reflects any origin and must not be used in production.
Recommended and Optional
| Variable | Default | Behavior |
|---|---|---|
TOKEN_ENCRYPTION_KEY | unset | Encrypts 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_SCOPES | openid profile email offline_access | Scopes 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_ID | unset | Zitadel organization ID used as the fallback tenant for instance-level admins (see Step 1). |
ZITADEL_MANAGER_KEY | unset | Base64-encoded Zitadel service-user key JSON (see Step 1). Unset: admin key management degrades to self-service only. |
ADMIN_ROLE_KEY | customer_admin | Zitadel role key that grants cross-principal key management. |
AGENT_PROVISIONING_CLIENT_IDS | unset | Comma-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_REGISTRATION | false | "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_URL | unset | The 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_ISSUER | derived | iss 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_AUDIENCE | istari-data-platform | aud claim of issued tokens. |
OIDC_CLIENT_SECRET | unset | Alternative to OIDC_PRIVATE_KEY if the Zitadel app uses Basic authentication. |
OIDC_SUBJECT_CLAIM | sub | ID-token claim used as the stable user identifier. |
REFRESH_TOKEN_TTL | 720h (30 days) | Refresh-token lifetime (Go duration, e.g. 168h). Must be positive. |
MAX_SESSION_DURATION | 720h (30 days) | Maximum session lifetime; refresh tokens never outlive the session. Must be positive. |
Advanced
| Variable | Default | Behavior |
|---|---|---|
HOST / PORT | 0.0.0.0 / 8000 | Listen address. |
AUDIT_SYSLOG_HOST, AUDIT_SYSLOG_PORT | unset | Forward audit events to a SIEM over TLS syslog. |
AUDIT_FORMAT | unset | Audit event format. |
AUDIT_FALLBACK_PATH | unset | Local file fallback for audit events when syslog is unreachable. |
AUDIT_TLS_SKIP_VERIFY | false | Skip TLS verification for the syslog connection (testing only). |
OTEL_ENABLED | false | true exports OpenTelemetry traces over OTLP/HTTP; the standard OTEL_EXPORTER_OTLP_ENDPOINT and related OTEL_* variables are honored. |
OTEL_SERVICE_NAME | identity-service | OpenTelemetry 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.
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.
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:
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.
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:
| Variable | Value |
|---|---|
FILE_SERVICE_FEATURE_FLAGS__IDENTITY_ROUTER_ENABLED | true |
FILE_SERVICE_IDENTITY_ROUTER_URL | https://api.<customer_istari_fqdn>/identity |
FILE_SERVICE_IDENTITY_ROUTER_SECRET | The registry's client credentials blob (base64 {clientId, keyId, key}) |
Frontend — add to the frontend secret (see Istari Platform Installation):
| Variable | Value |
|---|---|
VITE_IDENTITY_ROUTER_ENABLED | "true" |
VITE_IDENTITY_ROUTER_AUTHORITY | https://api.<customer_istari_fqdn>/identity |
VITE_IDENTITY_ROUTER_CLIENT_ID | The frontend's client ID (generated during client registration) |
Secure Connection Service — if deployed (see Secure Connection Service), add to the SCS secret:
| Variable | Value |
|---|---|
ISTARI_DIGITAL_IDENTITY_ROUTER_ENABLED | "true" |
ISTARI_DIGITAL_IDENTITY_ROUTER_URL | https://api.<customer_istari_fqdn>/identity |
ISTARI_DIGITAL_IDENTITY_ROUTER_AGENT_KEY | The 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:
| Variable | Value |
|---|---|
ISTARI_DIGITAL_IDENTITY_SERVICE_ENABLED | "true" |
ISTARI_DIGITAL_IDENTITY_SERVICE_ISSUER | https://api.<customer_istari_fqdn>/identity |
ISTARI_DIGITAL_IDENTITY_SERVICE_CLIENT_ID | A client ID for the MCP service |
ISTARI_DIGITAL_IDENTITY_SERVICE_CLIENT_SECRET | A generated secret for that client |
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).