Skip to content
Talk to our solutions team

Troubleshooting

Three error envelopes ship. Identify which one you are holding before you diagnose anything.

Where the failure happenedBodyContent type
/auth/*, /superadmin/*, /internal/session/* handlers{"error": "…", "code": "…"}application/json
/rest/*, /anon/rest/*, /admin/*, /schema/* handlers{"error": "…", "code": "v2.…", "details": {…}}application/json
The chassis token middleware, before any handler ranthe literal string Unauthorized, no codetext/plain
The tenant middleware, before auth ranthe literal string invalid tenant, HTTP 400, no codeapplication/text

A plain-text Unauthorized on /rest or /admin is the single most common failure on the entity surface and it carries no diagnostic at all — jump to The token is rejected by another service.

Key your client on the (status, code) pair. These codes ship on more than one status or more than one subsystem:

CodeMeanings
no_tenant400 from an auth handler; 401 from the token middleware when a credential was presented without CPET headers
unknown_realm400 at login; 401 at MFA verify (the realm vanished between the two calls)
exchange_failed401 on the OAuth callback; 500 on POST /internal/session/exchange
invalid_session401 for a WebAuthn challenge session; 401 for a managed-session reference
session_failed, session_persist_failed, rotate_failed, list_failedone code, two unrelated flows each — the route disambiguates

engine_unavailable always reads could not reach datastore, whatever actually failed. The pool, config and vault error text only exists in the server log.

The service will not start, or starts and serves nothing

Section titled “The service will not start, or starts and serves nothing”
You seeMeansConfirmFix
invalid/empty port provided, exit 1port absent in both the flag and the YAMLgrep port .iam.yamlSet port (5030 in every shipped profile) or pass -p
failed to load Kisai TLS Config / failed to load TLSConfigzerotrust is true whenever the key is absent, and the listener then demands cert materialThe key is missing from a hand-written bootstrapSet zerotrust: false for a local boot, or supply svccert + svckey
error: either config.apikey or svccert should be presentconfig.url set with no way to authenticate to the config serverAdd config.apikey, or run with a service certificate
error: either servicediscovery_apikey or svccert should be presentservicediscovery_url set with no auth pathSame
<what> not ready (attempt N): … — retrying for up to <remaining>, then a hard failureA boot dependency (config, vault, meta, service discovery) never came up inside boot.retry_window (default 90s)Reach the dependency URL from the same hostFix the dependency, or raise boot.retry_window
Boot panics on startThe embedded schema, an /admin mount, or a scripted auth hook failed to compileThe log line just before the panic names whichRoll the deployment back; the binary or its embedded assets are broken
Only /ready and /health answer; every other route 404sv1-only: true. There is no legacy surface to fall back to, so the gate mounts nothingcurl localhost:5030/auth/login returns 404 while /ready returns 200Remove the key. It cannot be re-enabled at runtime — restart
Two builds answering the same port at randomreuseport defaults to true, so a second process binds the same host:port without an errorlsof -i :5030 shows two PIDsStop the stale process; there is no address-in-use to warn you

400 invalid tenant, or 500 engine_unavailable

Section titled “400 invalid tenant, or 500 engine_unavailable”
You seeMeansConfirmFix
400 + plain-text invalid tenant on every route except /ready and /healthThe four CPET headers are missing, incomplete, or neither the full CPET join nor the bare X-Tenant value resolves to a configured tenant nodeRe-send with all four headersSend X-Customer, X-Product, X-Env, X-Tenant; add the node to the tenant hive or the config server
400 invalid tenant with all four headers setThe header is X-Env, not X-EnvironmentPrint the request headersRename the header
500 engine_unavailable, could not reach datastoreThe CPET passed the existence gate but no engine could be built: no datastore/dbpool for that CPET, an unreachable database, bad credentials, or a config refresh in flightServer log at info names the failing stepDeclare the CPET’s datastores and dbpools, provision it, fix the credential
500 engine_unavailable for a product you just addedAn unknown product does not fail at the middleware. Product registration is a cheap empty bucket and a missing tenant section resolves as empty, so the first thing that actually fails is the pool buildLog shows the pool or datastore resolution failing, not a tenancy errorAdd the tenant node for that full CPET. There is no product allow-list to edit
Log: no v2 pool factory for DBPool type "<t>"The dbpool declares a backend other than PostgreSQLRead the dbpool’s typeOnly postgres has a pool factory here
Log: pool ping: … authentication failure with a correct-looking passwordA dbpool password is tried against vault first and falls back to being used literally. With no vault client registered, a vault secret name is sent as the passwordCheck whether vault.url is setSet vault.url, or inline the literal password
Log: no DStore config for "<tenant>":"<ds>"The tenant node has no datastore by that nameAdd the datastore, or send ?datastore=<name>
403 v2.no_tenant on /rest or /schemaThe same missing-header condition the auth surface reports as 400 or 401Send the CPET headers
A request for a CPET that does not exist still reaches the engineThe middleware’s existence gate is a two-key probe: the full CPET, then the bare X-Tenant value. A hive node named for the bare tenant satisfies the gate even when the full CPET has no nodeCompare the node path against the full four-segment keyKey production nodes by the full CPET

Header resolution and the per-CPET scope model are described in Tenancy.

Login is refused for a credential you know is valid

Section titled “Login is refused for a credential you know is valid”
You seeMeansConfirmFix
401 invalid_credentialsWrong password, no such identity, an inactive identity, or an identity row with no password hash. The four are deliberately indistinguishable, and the miss path runs a dummy argon2 verify so timing does not leak eitherRead the identity row over /rest/users with an admin tokenReset the password, activate the account, or check that the row carries a hash
400 unknown_realmThe realm named in the body is not in this CPET’s composed behavior configList the realms the tenant actually hasFix the realm value, or deliver the product’s realm declarations to this CPET (meta.url / meta.localdir)
403 provider_not_allowed, realm <name> does not allow password loginThe realm exists but does not list password among its providersRead the realm’s providers listAdd password, or use the realm that has it
400 bad_identity_typeidentity_type names a column the realm’s entity does not declare as a login identifier. The message lists the ones it doesUse a declared identifier, or declare the column on the entity
403 hook_denied with an unfamiliar messageA product auth hook aborted the flow. The message is hook-authored and is not a stable stringSearch the product’s hook definitions for the messageFix or remove the hook
429 login_abuse + Retry-AfterThe per-(tenant, caller IP) login bucket is exhausted (capacity 30, refill 0.5/s)Back off. The limiter is in-memory per process, so N replicas give N× the limit, and there is no config key to tune it
409 session_limitA managed CPET reached session.max_concurrent_sessions and its session.on_limit is rejectRead the tenant’s session: blockSet on_limit: evict_oldest, or raise the cap
503 policy_unavailableThe tenant’s session policy could not be resolved, and the service refuses to guess stateless vs managedLog names the behavior-composition failureFix the behavior layer; a downgrade to standard would silently weaken a managed tenant
500 session_service_unavailableThe CPET is configured for managed sessions but no session service is wired into this processDeployment wiring problem — it refuses loudly rather than issuing a stateless token
500 issue_failed, could not issue refresh token after an upgradeThe live refresh-token table predates the binary’s schema. The access token minted; the refresh insert hit a missing columnCompare the table against a generated planiam.svc data plan generateshowapply per tenant. Refresh tokens issued before the migration 401 once
500 sign_failedThe tenant’s signing key could not sign the claim setLog carries the real errorBad bring-your-own PEM, a missing signing-key table, or an unreachable vault
500 user_query_failed, lookup failedThe identity lookup query itself errored — distinct from returning no rowsMissing identity table or an engine fault; run the migration
503 superadmin_not_deployed on a login with realm: superadminThe control plane for this product and environment is not deployedAdd the <customer>:<product>:<environment>:superadmin node and bootstrap it

Only /auth/login returns a named 429 code. Every other rate-limited route — MFA verify, password reset, magic link, POST /auth/token, API-key issue — returns the limiter’s own 429 body with no code. POST /superadmin/impersonate is wrapped under a profile the default limiter does not declare; do not assume it is limited.

You seeMeansConfirmFix
401 invalid_mfa_token, wrong token typeA full access token was sent to /auth/mfa/verify where the mfa_token from the login response belongsCompare against the mfa_token field of the login bodySend the pending token
401 invalid_mfa_token, MFA token expiredToo long between login and verifyLog in again
401 invalid_token, MFA-pending tokens cannot be used for protected requestsA pending token was presented to an ordinary protected route. The middleware refuses type: mfa_pending everywhere except MFA verificationComplete /auth/mfa/verify and use the token it returns
401 unknown_realm at verify (400 at login)The realm carried on the pending token no longer existsThe tenant’s realm declarations changed mid-flowRestart the login
401 invalid_codeThe code did not validateCheck client clock skew against the ~30-second stepResync the device clock
401 code_reused, this code was already used; wait for the next oneThe code was consumed within its time stepWait for the next code — replay is refused by design
401 mfa_not_enabled at verifyThe user has no enabled factorStale client flow, or the factor was disabled between login and verify
400 no_pending_setup on /auth/mfa/totp/enableEnrolment was never started, or the pending setup expired or was consumedCall /auth/mfa/totp/setup first
409 already_enabledTOTP is already on for this userDisable first — and disabling requires a current code, so a hijacked session alone cannot strip MFA

Details in Multi-factor authentication.

You seeMeansConfirmFix
401 with plain-text Unauthorized and no codeThe chassis token middleware rejected the credential before the handler. Signature failure, unknown key id, no configured issuer URL, a JWKS fetch timeout, or a tenant claim that disagrees with the request headers — all collapse to this one bodyRead the receiving service’s logWork through the rows below
Log: jwks: no configured URL for issuer "iam"The relying service has no jwks.issuers.iam entry, or it was nested under hives:jwks.issuers must be top level in the boot document, a sibling of host and portSet it to the externally reachable base URL of iam.svc
A token iam.svc just minted 401s on its own /rest or /adminThe same rule applies to itself — its entity surface goes through the same JWKS pathSame log lineiam.svc must set jwks.issuers.iam to its own reachable URL
401 tenant_mismatchThe token’s tenant claim names a different CPET than the request’s headers. Headers are authoritative and name the resource; the claim names the callerDecode the claim and compare against X-TenantRe-authenticate against the target tenant. There is no superadmin exception — a control-plane token is refused on a tenant surface exactly like any other mismatch
401 token_expired on a token that looks freshexp/nbf places it outside its window — or the token carries no exp at all, which is treated as forgeryDecode the claimsMint a new token; do not strip exp
401 auth_required where you expected invalid_tokenThe Authorization header did not match Bearer or ApiKey — both are prefix-matched, case-sensitive, single space. An unrecognised scheme is treated as no credential and passes the middleware with no principalByte-compare the headerFix the scheme and spacing
A rotated or revoked key keeps verifying during an issuer outageJWKS resolution prefers availability: cached for 5 minutes, background-refreshed to 24 hours, and past 24 hours a failed refetch still honours the cached keyTreat key rotation as eventually consistent; revoke sessions, do not rely on key removal
A receiving service accepts expired tokens indefinitelyIt called the signature-only verification entry point, which does not check exp/nbf, and never added its own timeliness checkRead the relying service’s verification callUse the entry point that validates temporal claims, or enforce exp yourself
Log banner: SECURITY: jwks.disabled=true — this service is accepting tokens WITHOUT signature verificationThe signature bypass is on. Every identity claim is attacker-controlled; only header-authoritative tenancy still holdsgrep jwks .iam.yamlRemove jwks.disabled outside development
GET /.well-known/jwks.json returns the wrong keysThe key set is per tenant and requires the CPET headers. Tenant A’s set never contains tenant B’s keysRe-fetch with the target tenant’s headersFetch per tenant

The full integration contract is in Verifying tokens in another service.

Managed sessions refuse to refresh or exchange

Section titled “Managed sessions refuse to refresh or exchange”
You seeMeansConfirmFix
403 managed_session on POST /auth/refreshThis CPET uses managed server-side sessions; refresh tokens do not exist for it. Note the status — SDKs that treat 403 as permanently forbidden will not fall backRead the tenant’s session.presetExchange the opaque reference at POST /internal/session/exchange instead
401 invalid_session, session invalid or expired at exchangeUnknown, expired (idle or absolute) or revoked — deliberately one answer for all three, so it is not an oracleCheck session.idle_timeout_s and session.absolute_expiry_sLog in again
404 unknown_session on POST /internal/session/rotateNo managed session with that control-plane id. Rotate returns 404 where exchange returns 401 for the same conditionThe id used by revoke, rotate and list is the control-plane row id, never the opaque session_id credential
A browser never sends the session cookieThe kis_session cookie is set HttpOnly, Secure, SameSite=Lax. Over plain HTTP a browser drops itWatch the Set-Cookie headerServe over TLS in development, or read session_id from the JSON body
Login on a managed CPET still returned token and refresh_tokenThe managed branch is wired into password login only. MFA verify, magic link, WebAuthn, OAuth and refresh still mint stateless tokens on a managed CPETCompare the login response shape across flowsRoute managed tenants through password login until the other flows carry the branch
Tokens minted under preset: fedramp cannot be verified anywhereThe preset parses and selects the v3 crypto suite, but v3 verification is not wired. Every token this service mints is PASETO v4.public (Ed25519) regardless of crypto_suiteUse standard or hipaa

Session models, timeouts and revocation live in Sessions.

You seeMeansFix
503 webauthn_disabled, WebAuthn not configured, on all four /auth/webauthn/* routesThe WebAuthn implementation is never enabled in the deployable binary. There is no configuration key that turns it onNone. Do not build a passkey flow against this deployment
404 unknown_provider, OAuth provider not configured, on /auth/oauth/:provider/start and /callbackNo OAuth provider is registered in the deployable binary, so every provider name is unknown. Declaring type: oauth in a provider declaration wires nothing — a provider declaration carries only name, type and template, no credentials, and nothing reads type or template at request timeNone. The route answers 404 for every name
You cannot find an OTP login endpointThere is no OTP provider — no SMS, email or WhatsApp one-time-code login. A user_otp table exists in the schema and no code reads itUse magic link for an emailed one-time code

See OAuth and federated identity and Magic link, WebAuthn and passkeys for what each surface does answer.

You seeMeansConfirmFix
404 v2.entity_not_found for an entity you declaredThe entity is not in this CPET’s composed schema — the product or tenant layer that declares it was not delivered, or a feature gate removed itGET /schema/entities for that CPETDeliver the layer; check realmconfig.enable.agents/bots/delegations — setting one false removes the entity from the schema entirely
404 on /admin/realm or /admin/tenantOnly two admin aliases are mounted: /admin/user and /admin/role. Realms are configuration, not rows, so the realm entity was removedUse /rest/<entity> or the control-plane routes
404 on /schema/<entity>The path is /schema/entities/:entityCorrect the path
A raw database or engine error in the error fieldThe entity surface returns engine error text verbatim and classifies by status: 403 when an access rule denied, 422 on validation, 404 when the row is absent, 500 otherwiseThe status carries the classification, not the codeRead the status first; do not parse the message
An entity is readable with no credential at allThe full anonymous mirror /anon/rest/* is mounted. Those requests reach the engine with no user id and no roles — only each entity’s own access: rules stop themcurl the entity under /anon/rest/ with no Authorization headerGive every extension entity explicit access: rules
A non-admin can write through /admin/user or /admin/roleThose mounts declare an admin rule, but no route authorizer is registered in this service, so the route-level check never fires. Access control there rests entirely on the users and role entities’ own access rulesEnforce it with entity access rules; see Authorization model
501 v2.search_text_not_supportedFree-text search needs a search backend this service does not wireSend a structured JSON filter
501 v2.file_storage_unconfigured / v2.file_downloader_unconfiguredNo file storage or downloader is registered hereDo not use multipart create or ?download=true against this service
429 v2.rate_limitedThe entity-surface limiter, keyed per (tenant, user, profile) — a different limiter from the /auth/* one, applied after authenticationBack off
You seeMeansConfirmFix
403 v2.requires_superadmin on plan apply, seed run, lock releaseWorking as intended. Apply, Seed, Retention, Locks and Maintenance are raised to superadmin on this service because the DDL touches signing-key and credential tables. Read and plan-generate stay at adminUse a control-plane token, or run the CLI out of band
schema "…" does not exist after a bootstrapdata bootstrap creates the PostgreSQL schema, never the databaseConnect to the database directlyCreate the database out of band; it is infrastructure, not this service’s job
Tables landed in a schema you did not expectWhen a dbpool omits schema:, the physical schema is the tenant slot name, not the service nameRead the dbpool declarationDeclare schema: explicitly
A plan refuses to applyThe plan’s maximum risk tier exceeds --confirm-risk (default low)iam.svc data plan show <plan-id>Re-run with --confirm-risk medium or high after reading the ops
invalid risk level "…" (use low / medium / high)Typo in --confirm-riskUse one of the three words
A migration hangs behind a lockA previous run died holding a coordination lockGET /admin/data/locksForce-release with DELETE /admin/data/locks/:resource (superadmin)
/admin/data/retention/* and /admin/data/materialize/* always failThe routes are mounted by the shared admin router but no provider backs them hereNot supported on this service
One tenant migrated, the rest did notEvery data verb takes exactly one --tenant. There is no all-tenants passLoop over your tenant inventory, and migrate the control plane separately with --tenant <customer>:<product>:<environment>:superadmin
config not ready: <why> from a CLI commandThe config client did not finish its first load inside 15 secondsReach config.url from the same hostFix reachability or the -f path
invalid --tenant "<x>" (want customer:product:env:tenant)Malformed CPETPass all four segments. Without --tenant the CLI targets default:iam:dev:default
Help text tells you to run iam migrate firstThere is no such commandThe verb is iam.svc data bootstrap
data seed run does nothingTwo different seed verbs exist: iam.svc seed creates privileged identities, iam.svc data seed run applies schema-declared reference data — of which this service declares noneUse iam.svc seed for the first admin

Control plane, bootstrap and impersonation

Section titled “Control plane, bootstrap and impersonation”
You seeMeansConfirmFix
403 already_bootstrapped on POST /auth/bootstrapThree causes, one answer: bootstrap.token is unset (the endpoint is disabled entirely), the token did not match, or a superadmin already existsCheck whether bootstrap.token is configuredSet bootstrap.token, or create the first operator with iam.svc seed
503 superadmin_not_deployedThe control plane for this product and environment has no hive binding, or its schema was never bootstrapped. The plane is per customer, product and environment — not one per deploymentLook for a node whose tenant slot is superadminAdd the node, then iam.svc data bootstrap --tenant <customer>:<product>:<environment>:superadmin
400 reserved_or_invalid_name registering a tenantThe slug is empty, is not a valid four-segment key, or uses superadmin or shared in any segmentThose two names are reserved everywhere, in every deployment
403 not_an_operator on impersonateA tenant identity, or an operator of another product’s plane, called a control-plane operationUse the right plane’s operator token
403 role_requiredThe operator holds neither impersonate-readonly nor impersonate. Neither is implied by any other roleRead the operator’s rolesGrant the role explicitly; read-write requires the stronger one
403 not_granted, you do not manage that tenantNo grant over the target tenant — or the tenant does not exist, deliberately conflated so the endpoint cannot enumerate tenantsAdd the grant, and verify the tenant separately
403 impersonation_not_allowedThe target user has not opted in. An operator cannot write that field, so cannot self-authoriseRead the user rowThe user or their tenant admin must opt in
400 reason_requiredreason is mandatory — the audit record cannot be reconstructed laterSupply it
GET /superadmin/tenant is missing tenants you configuredIt lists tenants touched in this process, so the answer differs per replicaCompare against the config hiveThe hive or config server is the authoritative inventory

Operator roles, grants and the plane pin are covered in Superadmin and Impersonation.

You seeExplanation
error: svr is nil in shutdown on every SIGTERMIn-flight requests are not drained — only the listener closes. Drain connections upstream before stopping an instance
ready:true while every request 500sBoth probes are process-liveness flags. Neither consults the database, the config client or any tenant
Password reset and magic link return 200 but nothing is deliverednotify.url is unset, so the sink is a no-op and codes are dropped. The endpoints still succeed; the gap appears as sink_error in the flow’s after-events
compliances: encrypted fields are readable plaintext in the databaseField encryption is installed only when a vault client is registered. With vault.url empty the hooks are simply absent — no error. Turning it on later needs a data migration; existing plaintext rows stay plaintext until rewritten
No metrics and no traces despite the configTelemetry is never initialised on this boot path. telemetry.enabled and enablecache are inert keys in the shipped profiles, and the metric names in the source are recorded by nothing. Observability here is structured logs
A JWKS response or a credential list looks truncatedKey sets cap at 100 entries and WebAuthn credentials at 50, silently
A 403 after a config change took a whole tenant offlineA malformed behavior file, a malformed tenant overlay row, or a reserved claim name under token.claims/token.claims_from refuses the engine build for that CPET. Reserved names: iss, sub, tenant, realm, roles, iat, nbf, exp, type, scope, act
A revoked user still has a working access tokenStateless tokens are not revocable — they live out their TTL (15 minutes by default). Only managed sessions revoke instantly
Refresh-token rows accumulate foreverRotation tombstones have no retention sweep
A signing key never rotatesThere is no rotation endpoint and no schedule

For the complete code table see Error codes; for exact request and response shapes see the endpoint reference.