Skip to main content

Configuration

ThunderID's behavior is controlled by a combination of default configurations and deployment-specific overrides. This page covers all available configuration options and how to customize them.

Configuration System​

ThunderID uses a two-tier configuration system:

  1. Default Configuration. Provides sensible defaults for all settings. These are built into ThunderID.
  2. Deployment Configuration. Located at deployment.yaml in your ThunderID installation directory. This file overrides specific defaults for your deployment.
note

Only settings you want to override need to be specified in deployment.yaml. All other values will use the built-in defaults.

tip

ThunderID must be restarted after any configuration change.

Server Configuration​

Controls the ThunderID server's network settings and identity.

SettingDefaultDescription
server.hostnamelocalhostThe hostname or IP address the server binds to
server.port8090Port the server listens on
server.http_onlyfalseIf true, disables HTTPS and uses HTTP only (not recommended for production)
server.public_url(derived from server hostname, port and protocol)The public URL clients use to reach the server, if it differs from the bind address. Derived when unset.
server.identifierdefault-deploymentUnique identifier for this deployment instance. Every stored resource is scoped by it, so changing it on a server that already holds data hides that data rather than renaming it.
gateway.max_gateways1Guards how many gateways this deployment administers, through the Gateway API (POST /gateways) or a gateway declared in a file. A standalone deployment pairs with one; raise it only where something else manages the fleet, or set it to 0 for a deployment that administers none. A registration beyond the number is refused with 409. This is a guard rather than a guarantee: two registrations arriving at the same moment can leave one more gateway than configured, so do not rely on it as a licensing or security boundary.
gateway.max_versions3How many captured configuration versions this deployment keeps (POST /configuration-versions). Capturing one beyond the number removes the oldest, except a version some gateway holds or could be reverted to. Those are kept even when that leaves more versions than the number, so applying and reverting keep working whatever it is. The number is never less than one.
gateway.allow_private_addressesfalseWhether a gateway may be registered at, and reached on, a loopback, link-local or private address such as localhost, 10.0.0.5 or 169.254.169.254. Off, a base URL naming one is refused with 400 (GTW-1027), and a hostname that resolves to one is refused when the gateway is called, so this deployment's calls, which carry the gateway's key, cannot be pointed at something inside its own network. Turn it on for a local or single-host setup whose gateways sit on such addresses. A base URL must be https and carry no credentials whatever this is set to.
gateway.removal_confirmation_threshold5How many resources an apply or revert may remove from a gateway without being confirmed. One that would remove more is refused with 409 (GTW-1030) unless the request sets confirmRemovals; a dry run shows the removals first. A rollback to a much smaller version is the usual case. 0 turns the check off.
gateway.storecompositeWhere gateways are read from. mutable is the database alone, and gateways declared in files are not read. declarative is files alone, and registration through the API is refused. composite is both: listings merge the two, registration writes to the database, and a gateway declared in a file cannot be changed or removed through the API, because the file is read again on every start and would undo it.
notification.template.storecompositeWhere notification templates are read from. mutable is the database alone, and templates are editable through the Console and API. declarative is files alone, and writes through the API are refused with 403. composite is both: file-declared templates are read-only and are merged with editable database templates.

Resources are stored against this identifier, so it is fixed for the life of a deployment's data.

Every read and write acts for one deployment: the one a request carries, which on a server that holds one deployment is always server.identifier. That covers every stored resource, the gateways a deployment registers, its configuration versions, the version each gateway runs and the changes a gateway is set to leave alone. A server built to serve several deployments from one process keeps each one's gateways and versions apart by that identifier, so none sees another's.

One registered gateway is the default gateway, marked isDefault in the Gateway API. The first gateway registered through POST /gateways becomes the default. Registering another with isDefault: true moves the default to it, so no more than one gateway is the default at a time. The Console shows the default gateway's baseUrl in the runtime endpoints it lists for applications and agents. With no default gateway, it shows the server's own URL. A gateway declared in a file is never the default.

When server.public_url is set, the gate_client settings below default to the corresponding hostname, port, and protocol from that URL. Most deployments only need to configure the server section.

Gate Client Configuration​

Configures the connection to ThunderID Gate (the login UI). Every setting is optional. By default, the gate_client settings are derived from server.public_url, so you only need to configure this section when Gate is hosted separately from the server.

SettingDefaultDescription
gate_client.hostname(server's hostname)The hostname where ThunderID Gate is hosted
gate_client.port(server's port)Port where ThunderID Gate is accessible
gate_client.scheme(server's scheme)Protocol scheme (http or https)
gate_client.path/gateBase path for ThunderID Gate

TLS Configuration​

Controls HTTPS/TLS settings for secure communication.

SettingDefaultDescription
tls.min_version1.3Minimum TLS version to accept (1.2 or 1.3)
tls.cert_fileconfig/certs/server.certPath to TLS certificate file
tls.key_fileconfig/certs/server.keyPath to TLS private key file
Self-Signed Certificate

For quickstart and development, ThunderID generates a self-signed certificate at config/certs/server.cert during setup. For production, replace it with a certificate from a trusted Certificate Authority.

Logging Configuration​

Controls the verbosity and destination of server logs.

SettingDefaultDescription
log.levelinfoMinimum log level to emit (debug, info, warn, or error)
yaml
log:
level: "info"

If log.level is omitted, ThunderID defaults to info. An unrecognized value causes the server to exit at startup.

On Kubernetes, set the logging properties under configuration.log in your Helm values rather than editing deployment.yaml. See Logging.

Log Output​

By default ThunderID writes logs to the console (stdout) only. You can additionally, or instead, write them to a file. Console and file are independent outputs: enable either, both, or neither (with neither enabled, ThunderID falls back to the console so logs are never silently lost).

SettingDefaultDescription
log.output.console.enabledtrueWrite logs to stdout
log.output.file.enabledfalseWrite logs to a file
log.output.file.pathlogsDirectory for the log file. A relative path resolves under the ThunderID home directory; an absolute path is used as-is
log.output.file.file_namethunderid.logName of the active log file
log.output.file.formattextLog line format: text or json. Despite its position under file, this setting applies to console output as well, so set it to json to emit JSON to stdout without enabling file output
log.output.file.rotation.size.enabledfalseRoll the file over once it reaches max_size_mb
log.output.file.rotation.size.max_size_mb10Size (in MB, fractional values allowed, e.g. 0.5) at which the file is rotated
log.output.file.rotation.time.enabledfalseRoll the file over on a time schedule
log.output.file.rotation.time.interval_days1Roll the file over every N days at local midnight
log.output.file.rotation.max_backups0Maximum number of rotated files to keep (0 keeps all)
log.output.file.rotation.max_age_days0Delete rotated files older than this many days (0 disables age-based deletion)
log.output.file.rotation.compressfalseGzip-compress rotated files
yaml
log:
level: "info"
output:
console:
enabled: true
file:
enabled: true
path: "logs"
file_name: "thunderid.log"
format: "text"
rotation:
size:
enabled: true
max_size_mb: 10
time:
enabled: false
interval_days: 1
max_backups: 20
max_age_days: 0
compress: false

With the configuration above, logs are written to <THUNDER_HOME>/logs/thunderid.log. When the active file reaches the configured size limit, it is renamed with a timestamp suffix (for example thunderid.log.2026-07-06-14-30-00) and a fresh file is started. Older rotated files are then pruned by the configured backup count and age limit.

Size and time triggers are independent and can run together: with both enabled, the file rolls whenever it exceeds the size limit or reaches the next time interval, whichever comes first. Time-based rotation rolls at local midnight and skips days with no log output (so idle days do not create empty files).

note

The format applies to every component's log lines, regardless of when that component was created. The exception is the lines emitted before the logging configuration is applied, during server home resolution and configuration loading. Those use the default text format on stdout, because the format itself is part of the configuration still being loaded.

note

This is the ThunderID application log (the INFO/WARN/ERROR lines the server emits). It is separate from the observability event file sink described in the Observability guide, which records structured domain events such as token issuance and flow execution.

Access Log​

ThunderID writes an access log line for every HTTP request. The Gate and Console frontend paths (/gate/ and /console/) are always excluded.

SettingDefaultDescription
log.access.exclude_paths[]Additional path prefixes to exclude, alongside the always-excluded /gate/ and /console/ paths

To exclude other paths, add their prefixes in deployment.yaml. For example, to also exclude the health probe endpoints:

yaml
log:
access:
exclude_paths:
- "/health/"

Database Configuration​

ThunderID uses four separate databases for different purposes. Each database can be configured independently.

Connection parameters are grouped under a type-specific sub-key (postgres, sqlite, or redis). Only type is a top-level field; all other settings belong under the matching sub-key.

Config Database​

Stores identity provider configurations, applications, and authentication flows.

SettingDefaultDescription
database.config.typesqliteDatabase type (sqlite or postgres)

database.config.postgres.* is only read when database.config.type: postgres:

SettingDefaultDescription
database.config.postgres.hostname""Database server hostname
database.config.postgres.port0Database server port
database.config.postgres.name""Database name
database.config.postgres.username""Database username
database.config.postgres.password""Database password
database.config.postgres.sslmode""SSL mode (disable, require, verify-ca, verify-full)
database.config.postgres.max_open_conns500Maximum number of open connections
database.config.postgres.max_idle_conns100Maximum number of idle connections
database.config.postgres.conn_max_lifetime3600Maximum connection lifetime in seconds
database.config.postgres.max_retries3Maximum retry attempts for transient errors
database.config.postgres.min_retry_backoff_ms50Minimum delay before retrying in milliseconds
database.config.postgres.max_retry_backoff_ms2000Maximum delay before retrying in milliseconds

database.config.sqlite.* is only read when database.config.type: sqlite:

SettingDefaultDescription
database.config.sqlite.pathdatabase/configdb.dbSQLite database file path
database.config.sqlite.options_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)SQLite connection options
database.config.sqlite.max_open_conns500Maximum number of open connections
database.config.sqlite.max_idle_conns100Maximum number of idle connections
database.config.sqlite.conn_max_lifetime3600Maximum connection lifetime in seconds
database.config.sqlite.max_retries3Maximum retry attempts for transient errors
database.config.sqlite.min_retry_backoff_ms50Minimum delay before retrying in milliseconds
database.config.sqlite.max_retry_backoff_ms2000Maximum delay before retrying in milliseconds

Runtime-transient Database​

Stores short-lived runtime data such as authorization codes, authorization requests, PAR requests, and nonces. The runtime-transient database supports three backend types: sqlite, postgres, and redis.

SettingDefaultDescription
database.runtime_transient.typesqliteDatabase type (sqlite, postgres, or redis)

database.runtime_transient.postgres.* is only read when database.runtime_transient.type: postgres:

SettingDefaultDescription
database.runtime_transient.postgres.hostname""Database server hostname
database.runtime_transient.postgres.port0Database server port
database.runtime_transient.postgres.name""Database name
database.runtime_transient.postgres.username""Database username
database.runtime_transient.postgres.password""Database password
database.runtime_transient.postgres.sslmode""SSL mode (disable, require, verify-ca, verify-full)
database.runtime_transient.postgres.max_open_conns500Maximum number of open connections
database.runtime_transient.postgres.max_idle_conns100Maximum number of idle connections
database.runtime_transient.postgres.conn_max_lifetime3600Maximum connection lifetime in seconds
database.runtime_transient.postgres.max_retries3Maximum retry attempts for transient errors
database.runtime_transient.postgres.min_retry_backoff_ms50Minimum delay before retrying in milliseconds
database.runtime_transient.postgres.max_retry_backoff_ms2000Maximum delay before retrying in milliseconds

database.runtime_transient.sqlite.* is only read when database.runtime_transient.type: sqlite:

SettingDefaultDescription
database.runtime_transient.sqlite.pathdatabase/runtime_transient.dbSQLite database file path
database.runtime_transient.sqlite.options_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)SQLite connection options
database.runtime_transient.sqlite.max_open_conns500Maximum number of open connections
database.runtime_transient.sqlite.max_idle_conns100Maximum number of idle connections
database.runtime_transient.sqlite.conn_max_lifetime3600Maximum connection lifetime in seconds
database.runtime_transient.sqlite.max_retries3Maximum retry attempts for transient errors
database.runtime_transient.sqlite.min_retry_backoff_ms50Minimum delay before retrying in milliseconds
database.runtime_transient.sqlite.max_retry_backoff_ms2000Maximum delay before retrying in milliseconds

database.runtime_transient.redis.* is only read when database.runtime_transient.type: redis:

SettingDefaultDescription
database.runtime_transient.redis.address""Redis server address in host:port format, for example localhost:6379
database.runtime_transient.redis.username""Redis ACL username. Leave empty if ACLs are not configured
database.runtime_transient.redis.password""Redis password or ACL user password
database.runtime_transient.redis.db0Redis logical database index (0 to 15)
database.runtime_transient.redis.key_prefix""Prefix applied to all Redis keys written by ThunderID, for example thunderid:
database.runtime_transient.redis.max_retries5Maximum retry attempts for transient Redis network and timeout issues
database.runtime_transient.redis.min_retry_backoff_ms10Minimum delay before Redis retries in milliseconds
database.runtime_transient.redis.max_retry_backoff_ms100Maximum delay before Redis retries in milliseconds
database.runtime_transient.redis.dial_timeout_ms5000Redis connection (dial) timeout in milliseconds
database.runtime_transient.redis.read_timeout_ms3000Redis read timeout in milliseconds
database.runtime_transient.redis.write_timeout_ms3000Redis write timeout in milliseconds

Redis Connection Requirements​

  • ThunderID requires Redis 6.0 or later.
  • ThunderID connects to a single Redis node. Cluster mode and Sentinel mode are not currently supported.
  • The default Redis port is 6379. Ensure the port is reachable from the ThunderID server.
  • ThunderID does not enforce TLS at the client configuration level. To encrypt traffic, place a TLS-terminating proxy in front of Redis and point database.runtime_transient.redis.address at the proxy endpoint.
  • If your Redis deployment uses Access Control Lists (ACLs), create a dedicated user and grant the following commands: GET, SET, DEL, EXPIRE, EVAL, EVALSHA. Set database.runtime_transient.redis.username and database.runtime_transient.redis.password accordingly.
  • ThunderID calls PING at startup to verify connectivity. The process terminates if the Redis server is unreachable.

Database Retry Behavior​

ThunderID applies exponential backoff with jitter for transient failures on non-transactional SQL read operations (Query path). Retry behavior is configurable per database via max_retries, min_retry_backoff_ms, and max_retry_backoff_ms under the relevant postgres or sqlite sub-key.

  • Retryable classes include transient connectivity errors (ErrBadConn, ErrConnDone, connection reset/refused), timeouts, deadlocks (40P01), and PostgreSQL resource/availability states (53xxx, 57P01, 57P02, 57P03, too-many-connections).
  • Non-retryable conditions include no-row outcomes (ErrNoRows), syntax/validation issues, and other permanent SQL conditions.
  • Write operations executed via Execute are not retried automatically to avoid accidental duplicate writes. If your write path is explicitly idempotent, add idempotency at the business layer (for example with deterministic IDs or upsert semantics).
  • Retry metrics are emitted through OpenTelemetry metric instruments (thunderid_db_retry_attempts_total, thunderid_db_retry_backoff_seconds, thunderid_db_operation_seconds), which can be exported to Prometheus via your OpenTelemetry collector pipeline.

Entity Database​

Stores user profiles and credentials.

SettingDefaultDescription
database.entity.typesqliteDatabase type (sqlite or postgres)

database.entity.postgres.* is only read when database.entity.type: postgres:

SettingDefaultDescription
database.entity.postgres.hostname""Database server hostname
database.entity.postgres.port0Database server port
database.entity.postgres.name""Database name
database.entity.postgres.username""Database username
database.entity.postgres.password""Database password
database.entity.postgres.sslmode""SSL mode (disable, require, verify-ca, verify-full)
database.entity.postgres.max_open_conns500Maximum number of open connections
database.entity.postgres.max_idle_conns100Maximum number of idle connections
database.entity.postgres.conn_max_lifetime3600Maximum connection lifetime in seconds
database.entity.postgres.max_retries3Maximum retry attempts for transient errors
database.entity.postgres.min_retry_backoff_ms50Minimum delay before retrying in milliseconds
database.entity.postgres.max_retry_backoff_ms2000Maximum delay before retrying in milliseconds

database.entity.sqlite.* is only read when database.entity.type: sqlite:

SettingDefaultDescription
database.entity.sqlite.pathdatabase/entitydb.dbSQLite database file path
database.entity.sqlite.options_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)SQLite connection options
database.entity.sqlite.max_open_conns500Maximum number of open connections
database.entity.sqlite.max_idle_conns100Maximum number of idle connections
database.entity.sqlite.conn_max_lifetime3600Maximum connection lifetime in seconds
database.entity.sqlite.max_retries3Maximum retry attempts for transient errors
database.entity.sqlite.min_retry_backoff_ms50Minimum delay before retrying in milliseconds
database.entity.sqlite.max_retry_backoff_ms2000Maximum delay before retrying in milliseconds

Runtime-persistent Database​

Stores SSO sessions, revoked tokens, consent records, and account access state (sign-in locks and failed-attempt counts). This database is required. ThunderID fails to start when database.runtime_persistent is not configured.

SettingDefaultDescription
database.runtime_persistent.typesqliteDatabase type (sqlite or postgres)

database.runtime_persistent.postgres.* is only read when database.runtime_persistent.type: postgres:

SettingDefaultDescription
database.runtime_persistent.postgres.hostname""Database server hostname
database.runtime_persistent.postgres.port0Database server port
database.runtime_persistent.postgres.name""Database name
database.runtime_persistent.postgres.username""Database username
database.runtime_persistent.postgres.password""Database password
database.runtime_persistent.postgres.sslmode""SSL mode (disable, require, verify-ca, verify-full)
database.runtime_persistent.postgres.max_open_conns500Maximum number of open connections
database.runtime_persistent.postgres.max_idle_conns100Maximum number of idle connections
database.runtime_persistent.postgres.conn_max_lifetime3600Maximum connection lifetime in seconds
database.runtime_persistent.postgres.max_retries3Maximum retry attempts for transient errors
database.runtime_persistent.postgres.min_retry_backoff_ms50Minimum delay before retrying in milliseconds
database.runtime_persistent.postgres.max_retry_backoff_ms2000Maximum delay before retrying in milliseconds

database.runtime_persistent.sqlite.* is only read when database.runtime_persistent.type: sqlite:

SettingDefaultDescription
database.runtime_persistent.sqlite.pathdatabase/runtime_persistent.dbSQLite database file path
database.runtime_persistent.sqlite.options_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)SQLite connection options
database.runtime_persistent.sqlite.max_open_conns500Maximum number of open connections
database.runtime_persistent.sqlite.max_idle_conns100Maximum number of idle connections
database.runtime_persistent.sqlite.conn_max_lifetime3600Maximum connection lifetime in seconds
database.runtime_persistent.sqlite.max_retries3Maximum retry attempts for transient errors
database.runtime_persistent.sqlite.min_retry_backoff_ms50Minimum delay before retrying in milliseconds
database.runtime_persistent.sqlite.max_retry_backoff_ms2000Maximum delay before retrying in milliseconds

Cache Configuration​

ThunderID includes both in-memory and Redis-backed caching to improve performance.

SettingDefaultDescription
cache.disabledfalseIf true, disables caching entirely
cache.typeinmemoryCache type (inmemory or redis). When cache.type is redis, ThunderID does not use the periodic cleanup routine.
cache.size1000Maximum number of cache entries
cache.ttl3600Default time-to-live for cache entries in seconds
cache.eviction_policyLRUCache eviction policy (LRU - Least Recently Used)
cache.cleanup_interval300In-memory only. Interval in seconds to clean up expired entries. Not used when cache.type is redis.
cache.properties[][]Per-cache overrides for name, disabled, size, ttl, and eviction_policy
cache.redis.address""Redis server address in host:port format
cache.redis.username""Redis username
cache.redis.password""Redis password
cache.redis.db0Redis database index
cache.redis.key_prefixthunderidPrefix added to all Redis cache keys
cache.redis.max_retries5Maximum retry attempts for transient Redis network and timeout issues
cache.redis.min_retry_backoff_ms10Minimum delay before Redis retries in milliseconds
cache.redis.max_retry_backoff_ms100Maximum delay before Redis retries in milliseconds
cache.redis.dial_timeout_ms5000Redis connection (dial) timeout in milliseconds
cache.redis.read_timeout_ms3000Redis read timeout in milliseconds
cache.redis.write_timeout_ms3000Redis write timeout in milliseconds

Cache Property Overrides​

Use cache.properties when you want to override cache behavior for a specific internal cache instead of changing the global cache settings for all caches.

Each item in cache.properties supports these fields:

FieldRequiredDescription
nameYesInternal cache name to override. The value must match the cache name exactly.
disabledNoDisables only that cache when set to true.
sizeNoMaximum number of entries for that cache. This setting applies to in-memory caches only.
ttlNoTime-to-live for that cache in seconds.
eviction_policyNoEviction policy for that cache. Supported values are LRU and LFU. This setting applies to in-memory caches only.

If you omit a field in a cache.properties entry, ThunderID falls back to the corresponding global cache.* setting.

That means you can override only the fields you need. ThunderID continues to use the global cache settings for any fields you leave unset.

ThunderID currently uses cache names such as:

  • ApplicationByIDCache
  • ApplicationByNameCache
  • OAuthAppCache
  • FlowByIDCache
  • FlowByHandleCache
  • CertificateByIDCache
  • CertificateByReferenceCache
  • EntityTypeByIDCache
  • EntityTypeByHandleCache
  • FlowGraphCache
note

FlowGraphCache is always in-memory. It caches process-local flow graph Go objects during flow execution, not shared system-level cache data.

note

When cache.type is redis, per-cache ttl and disabled remain useful. Per-cache size and eviction_policy do not affect Redis behavior because Redis manages memory and eviction independently.

Deployment-Scoped Entries​

Every cache entry belongs to the deployment the request acts for, the same deployment the stores read and write rows for. Two deployments served by one ThunderID process, or sharing one Redis server, never read or remove each other's entries, even when they hold entries under the same key.

  • Work that runs outside a request, such as a background task, acts for the deployment in server.identifier.
  • Clearing an in-memory cache removes its entries for every deployment. A Redis cache is not cleared; its entries expire after their time-to-live.

Redis Cache Configuration​

Set cache.type to redis and configure cache.redis.address to enable Redis-backed cache storage.

yaml
cache:
type: "redis"
ttl: 3600
redis:
address: "localhost:6379"
username: ""
password: ""
db: 0
key_prefix: "thunderid"
max_retries: 5
min_retry_backoff_ms: 10
max_retry_backoff_ms: 100
dial_timeout_ms: 5000
read_timeout_ms: 3000
write_timeout_ms: 3000
note

When Redis caching is enabled, ThunderID expires keys automatically based on TTL.

warning

If ThunderID cannot connect to Redis during startup, it disables the cache layer.

JWT Configuration​

Controls JWT (JSON Web Token) generation and validation.

SettingDefaultDescription
jwt.validity_period3600Default JWT validity period in seconds (1 hour)
jwt.audienceapplicationDefault audience claim for JWTs
jwt.preferred_key_iddefault-keyKey ID to use for signing JWTs
jwt.leeway30Clock skew tolerance in seconds for token validation

OAuth Configuration​

OAuth 2.0 and OpenID Connect settings.

SettingDefaultDescription
oauth.refresh_token.renew_on_granttrueIf true, issues a new refresh token on each access token grant. Set to false to reuse the existing refresh token until it expires
oauth.refresh_token.revoke_previous_on_renewtrueIf true, revokes the consumed refresh token after a successful renewal. Applies when renew_on_grant is enabled
oauth.refresh_token.validity_period86400Refresh token validity period in seconds (24 hours)
oauth.token_revocation.enabledtrueIf true, enables token revocation on the authorization server: the RFC 7009 endpoint (/oauth2/revoke), the implicit revocation triggers, and the check that rejects revoked tokens during introspection, the refresh grant, and token exchange. Setting it to false turns off all of them, not only the endpoint. See Token Revocation
oauth.revocation.token_family.on_explicit_revoketrueIf true, revoking a token through /oauth2/revoke also revokes every other token of the same authorization grant
oauth.revocation.token_family.on_refresh_replaytrueIf true, presenting a refresh token that has already been rotated out revokes every token of that grant. Requires oauth.refresh_token.renew_on_grant, since a refresh token can only be replayed once it has been rotated
oauth.revocation.token_family.on_code_replaytrueIf true, redeeming an authorization code a second time revokes every token issued from that code
oauth.token_exchange.token_familynoneWhether an exchanged token joins the subject token's authorization grant. none issues an independent token; inherit copies the subject token's token family, so revoking the grant also revokes the exchanged token
oauth.authorization_code.validity_period600Authorization code validity period in seconds (10 minutes)
oauth.authorization_request.validity_period3600How long the authorization request context stays valid while the user completes the login flow, in seconds (60 minutes). A non-positive value falls back to the default
oauth.dcr.enabledtrueIf true, enables the Dynamic Client Registration endpoint
oauth.dcr.insecurefalseIf true, allows insecure dynamic client registration (development only)
oauth.client_assertion.max_lifetime300Maximum lifetime of a private_key_jwt client assertion in seconds. The exp claim must not be later than iat plus this value, or the current time plus this value when iat is absent
oauth.client_assertion.max_iat_age60Maximum age of a private_key_jwt client assertion's iat claim in seconds. Assertions with an older iat are rejected
oauth.cimd.enabledtrueIf true, advertises client_id_metadata_document_supported in the authorization server metadata, so MCP clients that support Client ID Metadata Documents identify themselves with one. Set it to false if MCP clients should register through Dynamic Client Registration instead. It doesn't affect registering clients from a document in the Console. See Register an MCP Client from Its Metadata Document
oauth.allowed_auth_methods["client_secret_basic", "client_secret_post", "private_key_jwt", "none"]Client token endpoint authentication methods allowed during client registration
oauth.allowed_response_types["code"]OAuth response types allowed during client registration
oauth.allowed_grant_types["client_credentials", "authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange", "urn:openid:params:grant-type:ciba", "urn:ietf:params:oauth:grant-type:jwt-bearer"]OAuth grant types allowed during client registration
oauth.allow_wildcard_redirect_urifalseIf true, allows wildcard patterns in registered redirect URIs: * and ** in the path component, and * in the host component (label-internal, alphanumeric only). When false, only exact redirect URI matching is performed and registering a wildcard URI returns a 400 Bad Request error.
oauth.send_server_errors_to_clientfalseIf true, an authentication flow failure that maps to the OAuth server_error code is reported to the client, as RFC 6749 section 4.1.2.1 requires. If false, the authorization code flow shows the error page instead of redirecting to the client, and CIBA leaves the request pending so the polling client times out. Denials (access_denied) are always reported to the client and are not affected by this setting.
note

Set oauth.client_assertion.max_lifetime and oauth.client_assertion.max_iat_age together, each greater than 0. When both are unset, the iat and lifetime checks on client assertions are skipped.

note

Enabling oauth.allow_wildcard_redirect_uri affects all applications in the deployment. See Use Wildcard Redirect URIs for pattern syntax and matching rules.

Flow Configuration​

Authentication and registration flow settings.

SettingDefaultDescription
flow.max_version_history10Maximum number of flow versions to retain
flow.auto_infer_registrationtrueIf true, automatically infers registration from authentication flows
flow.executors(all built-in executors)Whitelist of built-in executor names to register at startup. Omit the key or leave the list empty to register every built-in executor. When set, only the listed executors are available; flows that reference other executors fail validation at startup. Unknown names cause startup to fail. Duplicate names are ignored.

Built-in Executors​

Use flow.executors to limit which built-in flow executors the server registers. This setting applies only to built-in executors. When you embed ThunderID and register custom executors programmatically, call RegisterExecutor on the registry returned by Initialize after startup; flow.executors does not affect custom executors.

Register all built-in executors (default):

Omit flow.executors or leave it empty:

yaml
flow:
max_version_history: 10
auto_infer_registration: true
# executors: [] # optional; omit or empty registers all built-in executors

Register a subset:

List only the executor names you need. Built-in executors not in the list are excluded:

yaml
flow:
executors:
- CredentialsAuthExecutor
- InviteExecutor

For the complete list of built-in executor names and configuration details, see Executor Details and Configuration.

User Configuration​

User management settings.

SettingDefaultDescription
user.indexed_attributes["username", "email", "mobile_number", "sub"]User attributes that are indexed for fast lookups

An indexed attribute's value can be a single scalar value or a list of scalar values. A list indexes each scalar element separately, so a user can be looked up by any one of them. Non-scalar elements in the list (for example, a nested object) are skipped and are not indexed. For example, a user with email set to ["primary@example.com", "secondary@example.com"] can be identified, searched, or filtered by either address.

A list can hold at most 100 scalar values for one indexed attribute. A request that exceeds this fails schema validation.

note

List values aren't checked for uniqueness, so two users can hold the same value in a list. A value that more than one user holds can't be used to identify either of them.

note

If the same attribute name is present in both schema attributes and system attributes, the system attribute's scalar value or values are indexed and the schema attribute's scalar value or values are not. If the system attribute has no scalar value to index (for example, it is empty), the schema attribute's values are indexed instead.

Declarative Resources​

Controls declarative configuration support.

SettingDefaultDescription
declarative_resources.enabledfalseIf true, enables declarative resource configuration

Resource Loading Precedence​

At startup, ThunderID resolves declarative resources using the following precedence. Only the first matching source is used. Sources are not merged.

PrioritySourceWhen active
1Single file passed via ./start.sh <file>--resources flag is set
2*.yaml/*.yml files placed directly in config/resources/ (not in subdirectories)Flag not set; at least one root-level YAML file exists
3Per-type subdirectories under config/resources/ (e.g. config/resources/groups/)Flag not set; no root-level YAML files found

If --resources is set, config/resources/ is not read at all, regardless of what files it contains. The two sources never merge.

Start ThunderID With a Resources File​

Pass a single multi-document YAML file to the server at startup as a positional argument to start.sh. The file is forwarded to the server binary as the -resources flag and read directly at startup.

bash
./start.sh resources.yml

With environment variable substitution:

bash
./start.sh resources.yml --env my.env

Each document in the file must declare its type with a resource_type: <type> field. Supported types:

text
application, connection, flow, group, layout, notification_template,
organization_unit, resource_server, role, theme, translation, user, user_schema

Example single file:

yaml
resource_type: application
name: My App
ouHandle: default
authFlowHandle: default-flow
---
resource_type: group
name: admins
description: Administrators
warning

When --resources is active, any files in config/resources/, including per-type subdirectories, are silently ignored. If you need files from the directory to be loaded, do not pass a --resources argument.

Resources Directory (No Startup Flag)​

If you do not pass a --resources argument, ThunderID automatically picks up multi-document YAML files placed directly in the config/resources/ directory (not in subdirectories). This is priority 2 in the loading precedence and requires no configuration change or restart argument.

Place a multi-document YAML file in config/resources/:

text
repository/
resources/
my-resources.yaml # loaded automatically at startup
groups/ # used only when no root-level YAML files exist
admins.yaml

The file must use the same resource_type: <type> document fields as the single-file startup format. On startup, ThunderID scans all *.yaml and *.yml files at that root level and loads every document whose resource_type matches a configured declarative resource type.

note

If any *.yaml or *.yml file is found at the config/resources/ root, per-type subdirectories (priority 3) are skipped entirely for that resource type.

Helm Deployment​

For Helm deployments, this setting is managed through declarativeResources.enabled in the Helm values. When enabled, the chart mounts declarative resource files from either a ConfigMap or Secret into the ThunderID config/resources directory.

Two mounting modes are available. Use the one that fits your resource layout.

Combined File Mode​

Use singleFile.keys to mount one or more combined YAML files (multi-document format with resource_type: <type> fields) directly under config/resources. Each key in your ConfigMap or Secret is mounted as <key>.yaml at that path. ThunderID scans the directory at startup and loads every document whose resource_type matches a configured declarative resource type.

yaml
declarativeResources:
enabled: true
readOnly: true
singleFile:
keys:
- resources # mounted as config/resources/resources.yaml
- extra-resources # mounted as config/resources/extra-resources.yaml
configMap:
name: declarative-resources

The same works with a Secret source (for files containing sensitive values):

yaml
declarativeResources:
enabled: true
readOnly: true
singleFile:
keys:
- resources
secret:
name: declarative-resources-secret

Individual Files Mode​

Use configMap.items (or secret.items) to mount individual files into typed subdirectories under config/resources. Each item maps a ConfigMap/Secret key to a specific path.

yaml
declarativeResources:
enabled: true
readOnly: true
configMap:
name: declarative-resources
items:
# Format 1: string shorthand; key and path are the same
- "connections/google.yaml"
# Format 2: explicit key/path mapping
- key: app1
path: applications/application1.yaml
# Format 3: per-item mountPath override (absolute path)
- key: org-default
path: organization_units/default.yaml
mountPath: /opt/thunderid/config/resources/organization_units/default.yaml
note

singleFile.keys and configMap.items / secret.items are mutually exclusive.

Notification templates (email and SMS) are managed at runtime and are also supported as declarative resources (resource_type: notification_template). See Notification Templates for authoring and Console management, and Import Declarative Resources for the declarative schema.

Resource Configuration​

Authorization resource settings.

SettingDefaultDescription
resource.default_delimiter:Default delimiter for resource hierarchies

Resource Sharing Configuration​

Controls whether a resource can reach an organization unit outside the tree that owns it.

SettingDefaultDescription
resource_sharing.allow_child_ou_cross_tree_sharingfalseIf true, an organization unit that has a parent can share a resource with a root organization unit in another tree. If false, only a root organization unit can share outside its own tree.

Organization units form separate trees. Sharing within a tree is always permitted and is not affected by this setting. A root organization unit can always share with another root OU. This setting governs the other case, where an organization unit that sits inside a tree shares outward. Keeping it false means a resource leaves a tree only by decision of that tree's root.

yaml
resource_sharing:
allow_child_ou_cross_tree_sharing: false

Observability Configuration​

Monitoring and observability settings. ThunderID supports three output backends: console, file, and OpenTelemetry (OTel). You can enable any combination of them independently.

Top-Level Settings​

SettingDefaultDescription
observability.enabledfalseIf true, enables the observability system. All output backends are inactive when this is false.
observability.failure_modelenientControls how initialization failures in subscribers are handled. lenient logs a warning and continues with the remaining subscribers. strict stops initialization and returns an error if any subscriber fails.

Console Output​

Writes events to standard output. Useful for local development and containerized deployments that collect stdout logs.

SettingDefaultDescription
observability.output.console.enabledfalseIf true, outputs observability events to the console
observability.output.console.formatjsonOutput format: json or text
observability.output.console.categories["observability.all"]Event categories to output. See Event Categories for valid values.

File Output​

Writes events to a local file. The file subscriber appends events in the configured format and creates the output directory automatically if it does not exist.

SettingDefaultDescription
observability.output.file.enabledfalseIf true, outputs observability events to a file
observability.output.file.file_pathlogs/observability/observability.log (relative to the installation directory)Path to the output file. The directory is created automatically.
observability.output.file.formatjsonOutput format: json
observability.output.file.categories["observability.all"]Event categories to write. See Event Categories for valid values.

OpenTelemetry Output​

Exports events as OpenTelemetry spans to an OTLP-compatible backend such as Jaeger, Grafana Tempo, or an OpenTelemetry Collector. Events with the same TraceID are grouped into a single trace.

SettingDefaultDescription
observability.output.opentelemetry.enabledfalseIf true, exports observability events as OpenTelemetry spans
observability.output.opentelemetry.exporter_typeotlpExporter type: otlp (gRPC, for production) or stdout (for debugging)
observability.output.opentelemetry.otlp_endpoint""OTLP gRPC endpoint, for example localhost:4317. Required when exporter_type is otlp.
observability.output.opentelemetry.service_name""Service name included in all emitted spans, for example thunderid-iam
observability.output.opentelemetry.service_version""Service version included in span resource attributes, for example 1.0.0
observability.output.opentelemetry.environment""Deployment environment included in span resource attributes, for example production
observability.output.opentelemetry.sample_rate1.0Fraction of traces to sample. 1.0 samples all traces; 0.5 samples half.
observability.output.opentelemetry.categories["observability.all"]Event categories to export. See Event Categories for valid values.
observability.output.opentelemetry.insecurefalseIf true, disables TLS for the OTLP connection. Use only in development environments.

Event Categories​

Use these values in any categories list to filter which events a subscriber receives:

ValueDescription
observability.allMatches all events regardless of type (default)
observability.authenticationToken issuance events, automatic account locks, and self-service unlocks from a recovery flow
observability.authorizationAuthorization-related events
observability.flowsAuthentication and registration flow execution events
observability.administrationAdministrative changes: operator unlock and server-config writes

Example​

The following deployment.yaml snippet enables console output and OpenTelemetry export to a local collector:

yaml
observability:
enabled: true
failure_mode: lenient
output:
console:
enabled: true
format: json
categories:
- observability.all
opentelemetry:
enabled: true
exporter_type: otlp
otlp_endpoint: "localhost:4317"
service_name: "thunderid-iam"
service_version: "1.0.0"
environment: "production"
sample_rate: 1.0
insecure: false
categories:
- observability.all

Crypto Configuration​

Cryptographic settings for encryption and signing.

Encryption​

SettingDefaultDescription
crypto.encryption.keyfile://config/certs/crypto.keyPath to encryption key file

Password Hashing​

SettingDefaultDescription
crypto.password_hashing.algorithmPBKDF2Password hashing algorithm
crypto.password_hashing.parameters.iterations600000Number of hashing iterations
crypto.password_hashing.parameters.key_size32Derived key size in bytes
crypto.password_hashing.parameters.salt_size16Salt size in bytes

Signing Keys​

Signing keys are configured as an array. Each key has the following properties:

SettingDescription
crypto.keys[].idUnique identifier for the key
crypto.keys[].cert_filePath to certificate file
crypto.keys[].key_filePath to private key file

Default Key:

yaml
- id: "default-key"
cert_file: "config/certs/signing.cert"
key_file: "config/certs/signing.key"

The key type under crypto.keys determines the algorithm in id_token_signing_alg_values_supported in the OIDC discovery document. RSA keys advertise RS256; ECDSA P-256, P-384, and P-521 keys advertise ES256, ES384, and ES512; Ed25519 keys advertise EdDSA. If multiple keys are configured, all resulting algorithms are included without duplicates.

Attribute Cache Configuration​

During an authorization flow, ThunderID caches the resolved user attributes in the runtime store and references them from tokens through the aci claim. These attributes are stored as plaintext by default. Enable encryption to store them encrypted at rest using the key from crypto.encryption.key.

SettingDefaultDescription
attribute_cache.encryption.enabledfalseWhen true, cached user attributes are encrypted (AES-GCM) before being written to the runtime store and decrypted on read. Requires crypto.encryption.key to be configured.
warning

Treat attribute_cache.encryption.enabled as a set-once value. It is applied on both write and read with no format detection, so changing it while cached entries still exist causes failures: after enabling it, entries written as plaintext can no longer be decrypted; after disabling it, encrypted entries are read without decryption. Before changing it, clear the attribute cache (or wait for all entries to expire), then restart.

Email Configuration​

ThunderID sends email through email providers that you create as connections, and each email step in a flow selects the provider it sends through. See Configure an Email Provider.

warning

If you are upgrading from an earlier release, remove the email.smtp block from deployment.yaml, or configuration.email.smtp from your Helm values. The server does not start while deployment.yaml has an email block, and the Helm chart does not render while configuration.email.smtp.host is set. To move those settings to an email provider, see Migrate From email.smtp.

Notification Configuration​

Controls notification delivery behaviour, including OTP generation parameters.

SettingDefaultDescription
notification.otp.length6OTP character length. Must be in [4, 10].
notification.otp.use_numeric_onlytrueIf true, OTPs use digits only; if false, OTPs use a mixed alphanumeric character set.
notification.otp.validity_period_seconds120OTP validity period in seconds. Must be in [30, 600].

Authentication Provider Configuration​

External authentication provider settings.

SettingDefaultDescription
authn_provider.rest.enabledfalseSet to true to register the REST authentication provider. When false, the rest of the rest.* settings are ignored.
authn_provider.rest.credential_types[]Credential keys routed to the REST provider instead of default. Valid keys: provisionedEntityID, passkey, otp, federated, magiclink, openid4vp, password, clientSecret, flowSecret.
authn_provider.rest.base_url""Base URL for REST authentication provider
authn_provider.rest.timeout10Request timeout in seconds
authn_provider.rest.correlation_id_headerX-Correlation-IDHeader used to forward the request's correlation ID (trace ID) to the REST provider. Set a different name if the provider expects one.
authn_provider.rest.security.api_key""API key for REST provider authentication

Correlation ID Propagation​

Each request is assigned a correlation ID (trace ID), which the server generates when the client does not supply one. ThunderID forwards this ID on outbound HTTP calls so a request can be traced across services. The REST authentication provider, the consent service, the flow HTTP request executor, and the Vonage notification provider all set the X-Correlation-ID header when it is missing. For the REST authentication provider, the header name is configurable via authn_provider.rest.correlation_id_header.

Example: forward the correlation ID under a custom header name:

yaml
authn_provider:
rest:
enabled: true
base_url: "https://my-authn-service"
correlation_id_header: "X-Trace-Token"

CORS Configuration​

Controls which browser origins may call the ThunderID API. Origins are stored in the server-config cors section, not in deployment.yaml. No origins are allowed by default.

FieldDescription
allowedOriginsList of allowed origins. Each entry is a literal origin (for example https://app.example.com) or a regex object ({ regex: "^https://...$" }) matched against the request Origin header. The literal "*" is rejected, and "null" matches the CORS null origin. Any origin not in the list is rejected.

Add origins in either of these ways:

  • Declarative - create config/resources/server_configs/cors.yaml, then restart:
    yaml
    name: cors
    value:
    allowedOrigins:
    - "https://app.example.com"
    - regex: '^https://[a-z0-9-]+\.staging\.example\.com(:[0-9]+)?$'
  • Runtime - send the new value to PUT /server-config/cors; it is applied immediately and merged with the declarative file:
    http
    PUT /server-config/cors
    Content-Type: application/json

    { "allowedOrigins": ["https://app.example.com"] }

Literal origins are matched after the request Origin is normalized. Scheme and host are case-insensitive, a trailing host dot is ignored, and IPv6 hosts use bracket form. Internationalized domain names are compared in ASCII form. Ports must match exactly, so list each port separately. Methods, headers, credentials, and the preflight cache TTL are fixed per route in code; only the origin list is configurable.

note

Anchor every regex with ^ and $ and escape literal dots. Unanchored patterns produce a startup warning, and invalid patterns stop the server at startup.

warning

The null origin is shared by sandboxed iframes, file:// and data: documents, and some redirects, so allowing "null" cannot identify the caller. List it only when you intend to trust those contexts. With credentialed responses, it lets any such page make authenticated requests.

Content Security Policy​

RUNNING FROM SOURCE WITH MAKE RUN

CSP headers are not enforced in development. The policy is applied only when static assets are served through the backend server binary.

Controls where the browser may load scripts, styles, images, fonts, and other resources for the ThunderID Console and Gate UIs. The policy is deny-first: any resource that is not explicitly allowed is blocked.

The CSP header is sent only on ThunderID Console (/console/) and Gate (/gate/) responses.

FieldDescription
reportOnlyWhen true (the default), the browser reports violations to its console but still loads the resource, using the Content-Security-Policy-Report-Only header. Set it to false to enforce the policy and block disallowed resources.
reportUriOptional endpoint the browser posts violation reports to, mapped to the report-uri directive. Use a relative path or an http/https URL.
directivesMap of directive name (for example img-src) to its list of allowed sources. A directive you list here replaces that directive's baseline value on every CSP path.
pathsList of per-path overrides. Each entry has a location prefix (for example /console/) and its own directives. The longest matching location applies, and its directives override directives, which override the baseline.

Deny-First Baseline​

Every directive you do not configure falls back to this baseline:

text
default-src 'none';
script-src 'self';
style-src 'self';
img-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'none';
form-action 'self'

A resource type without a matching directive (for example font-src) falls back to default-src 'none' and is blocked. On HTML responses, ThunderID Gate gets a fresh per-request nonce to the script-src and style-src-elem directives so its own inline scripts and styles load.

Overrides replace, not extend

Listing a directive replaces its baseline value completely. Include 'self' in your list when you still need same-origin resources. For example, img-src: ["https://cdn.example.com"] blocks same-origin images until you add 'self'.

Configure the Policy​

Set the policy in either of these ways:

  • Declarative - create config/resources/server_configs/csp.yaml, then restart.
    yaml
    name: csp
    value:
    reportOnly: false
    paths:
    - location: "/console/"
    directives:
    img-src:
    - "'self'"
    - "https://cdn.example.com"
    style-src-elem:
    - "'self'"
    - "https://fonts.googleapis.com"
    font-src:
    - "'self'"
    - "https://fonts.gstatic.com"
  • Runtime - send the new value to PUT /server-config/csp; it is applied immediately:
    http
    PUT /server-config/csp
    Content-Type: application/json

    {
    "reportOnly": false,
    "paths": [
    {
    "location": "/console/",
    "directives": {"img-src": ["'self'", "https://cdn.example.com"]}
    }
    ]
    }

Directives at the top level of directives apply to every CSP path, while a paths entry applies only to requests under its location and overrides the top-level value for that directive.

A PUT replaces the entire runtime layer, so include every runtime value you want to keep. ThunderID then merges that layer with the declarative csp.yaml per directive and path, and any directive you configure in neither layer stays at the deny-first baseline.

note

Directive names must match the pattern ^[a-z][a-z0-9-]*$, and no source may contain whitespace, a semicolon, or a comma. An invalid value is rejected with SCF-1003 ("Invalid server configuration value").

Default Resource Server​

Sets the resource server for permission-bearing token requests that omit the resource parameter. It is stored in the server-config defaultResourceServer section, not in deployment.yaml. When no default is configured, a request that contains permission scopes but omits resource fails with invalid_target. OIDC-only and scopeless requests are not bound to a resource server, so they use the application's default audience (token.accessToken.defaultAudience), or the client_id when it is unset.

FieldDescription
resourceServerIdThe ID of a registered resource server. It must reference an existing resource server, otherwise the update is rejected with SCF-1003 ("Invalid server configuration value").

Set it in either of these ways:

  • Declarative - create config/resources/server_configs/defaultResourceServer.yaml, then restart:
    yaml
    name: defaultResourceServer
    value:
    resourceServerId: "<resource-server-id>"
  • Runtime - send the new value to PUT /server-config/defaultResourceServer; it is applied immediately:
    http
    PUT /server-config/defaultResourceServer
    Content-Type: application/json

    { "resourceServerId": "<resource-server-id>" }
note

A declarative default takes precedence and locks the setting: while defaultResourceServer.yaml sets a value, runtime PUT /server-config/defaultResourceServer requests are rejected. Remove the declarative value to manage the default at runtime.

Flow Defaults​

Controls server-wide default flows and context expiry times for each flow type. These settings are stored in the server-config flow section, not in deployment.yaml.

Each flow type has two fields:

FieldDescription
<type>.defaultHandleHandle of the flow to use when the application and the organization unit do not pin a specific flow. Empty string means no server default, and the flow type is treated as not configured at the server level.
<type>.expirySecondsHow long (in seconds) a started flow context remains valid before it expires. Must be a positive integer.

Supported flow types are authFlow, registrationFlow, userOnboardingFlow, recoveryFlow, signOutFlow, and userDeletionFlow.

userDeletionFlow is resolved differently from the others: it names an administration flow that runs when a user is deleted from the Console, rather than a flow an inbound request starts. Its shipped default revokes the user's tokens and terminates their sessions before the record is removed. Deleting a user through the DELETE /users/{id} API removes the record only and does not run this flow. See Token Revocation.

The account unlock administration flows have their own types: userUnlockFlow and agentUnlockFlow. Like userDeletionFlow, each names an administration flow run against an existing identity, not a flow an inbound request starts.

Resolution order: When an inbound request starts a flow, the effective flow is resolved as follows:

  1. Application-level override (the flow ID pinned on the application or agent)
  2. Organization unit default (the flow ID configured on the owning OU)
  3. Server default from this section (the handle is looked up to find the flow ID)

If none of the layers supplies a value, registration, recovery, user-onboarding, and sign-out are left not configured (the feature is treated as disabled for that client). Authentication has no such fallback: a missing auth flow causes startup validation to fail.

Configure flow defaults in either of these ways:

  • Declarative - create config/resources/server_configs/flow.yaml, then restart:
    yaml
    name: flow
    value:
    authFlow:
    defaultHandle: default-flow
    expirySeconds: 3600
    registrationFlow:
    expirySeconds: 3600
    recoveryFlow:
    expirySeconds: 1800
    signOutFlow:
    expirySeconds: 1800
  • Runtime - send the new value to PUT /server-config/flow; it is applied immediately and merged with any declarative file:
    http
    PUT /server-config/flow
    Content-Type: application/json

    {
    "authFlow": { "defaultHandle": "default-flow", "expirySeconds": 3600 },
    "registrationFlow": { "expirySeconds": 3600 }
    }
note

Set defaultHandle only for flow types that have a corresponding flow in your deployment. The handle is validated when the configuration is written. An unknown handle or a handle belonging to a different flow type is rejected with SCF-1003. For optional types (registration, recovery, user-onboarding, sign-out), omit defaultHandle to leave the server-level default empty.

Account Access​

Controls the automatic account lock: how many failed sign-ins lock a user or an agent, and for how long. The policy is stored in the server-config accountAccess section. Only the user and agent categories can be configured; applications are never locked automatically, and an application block is rejected.

Only password and one-time-password failures are counted. A lock:

  • Refuses new sign-ins only through the method that failed, or through every method when lockGranularity is entity.
  • Does not end existing sessions or revoke tokens.

A refused sign-in by a locked identity states that the account is locked, without saying which method is locked or when the lock ends. An unknown identifier keeps its own answer. A wrong password or one-time password presented to an account held for that method gets the same answer as the correct one and is not counted. At the recovery step, a wrong code that the lock gives way to keeps its usual answer.

With observability enabled, every lock that forms, and every unlock, is published as an event. See Event Categories.

Settings​

These fields of a category (user, agent) control the lock. All are optional, so a layer states only what it changes.

FieldDescription
lockGranularityauthentication_method (default) locks only the sign-in method that failed. entity counts every lockable failure together and locks every sign-in method.
defaultThe policy for every sign-in method in the category (see below).

A policy has these fields:

FieldDescription
enabledTurns the automatic lock on or off. A policy that omits it is enabled when it sets any other policy field. An enabled policy, after it is laid over the base, must have threshold, failureWindowSeconds and lockDurationsSeconds, or the write is rejected.
thresholdFailed attempts that form a lock. Must be at least 1 while the policy is enabled.
failureWindowSecondsA failure more than this long after the previous one restarts the count. 0 never restarts it on time. Required while the policy is enabled.
lockDurationsSecondsThe lock duration for each consecutive lock, in seconds. The last value repeats. 0 locks until an administrator unlocks or the user completes account recovery. Must have at least one entry while the policy is enabled.
lockDecaySecondsHow long after a lock ends the consecutive lock count resets. 0 never resets it.
maxConsecutiveFailuresConsecutive failed attempts after which the lock does not expire. Only a successful sign-in with that method (any method with entity granularity), a completed recovery, or an unlock resets the count. 0 sets no limit; otherwise it must be at least threshold.

Every seconds value must be a whole number from 0 to the largest number of seconds a duration can hold.

Product Defaults​

The defaults ship as the account_access block of default.json, with the keys in snake_case (lock_granularity, lock_durations_seconds, and so on):

CategoryThresholdFailure windowLock durationsDecayConsecutive failures
user53600 s300, 900, 1800, 3600 s86400 s50
agent5900 s60, 300, 900 s3600 s50

Both categories default to authentication_method granularity.

Override the block in deployment.yaml, which is the base of the section:

yaml
account_access:
user:
default:
threshold: 3
lock_durations_seconds: [600, 3600]

Layers and Locking​

The effective policy is built from layers, read each time it is needed, so a change on any node applies without a restart:

  1. The base: the account_access block from default.json and deployment.yaml.
  2. A declarative document, when one exists, is laid over the base field by field. It locks the section: runtime writes are rejected until the declarative value is removed.
  3. Otherwise the runtime value is laid over the base field by field.

Within a category, a non-empty lockDurationsSeconds replaces the lower one. If the section cannot be read, the base applies.

Configure the Policy​

Set the section in any of these ways:

  • Declarative: create config/resources/server_configs/accountAccess.yaml, then restart. The section uses camelCase keys:
    yaml
    name: accountAccess
    value:
    user:
    lockGranularity: authentication_method
    default:
    threshold: 3
    failureWindowSeconds: 1800
    lockDurationsSeconds: [600, 3600]
  • Runtime: send the new value to PUT /server-config/accountAccess. It is applied immediately:
    http
    PUT /server-config/accountAccess
    Content-Type: application/json

    {
    "user": {
    "default": {
    "threshold": 3,
    "failureWindowSeconds": 1800,
    "lockDurationsSeconds": [600, 3600]
    }
    }
    }

A PUT replaces the entire runtime layer, so include every runtime value you want to keep. To remove the runtime layer and return to the base, send an empty value.

note

In a Helm deployment, set the base with the configuration.accountAccess chart value, in camelCase. The chart's values.yaml carries the product defaults, so override only the keys you need. The API and a declarative server_configs/accountAccess.yaml mounted with declarativeResources (see Helm Deployment) work as in any other deployment.

Validation​

An invalid value is rejected with HTTP 400 and SCF-1003 ("Invalid server configuration value"), and nothing is changed. The section rejects:

  • Unknown fields, including a misspelled key and an application block.
  • A lockGranularity other than authentication_method or entity.
  • A negative threshold or seconds value, and a seconds value above the maximum.
  • A negative maxConsecutiveFailures, or one above 0 and below the policy's threshold.
  • An enabled policy, after it is laid over the base, that cannot form a lock: it needs a threshold of at least 1, a failureWindowSeconds and a non-empty lockDurationsSeconds. Set enabled: false to switch the policy off.
  • Any write while a declarative accountAccess document exists.

The same rules check the base on its own at startup. An enabled policy in the account_access block of default.json and deployment.yaml must have its threshold, failure_window_seconds and lock_durations_seconds without any later layer. An invalid base stops the server from starting.

Identity Governance Provider​

SettingDefaultDescription
identity_governance_provider.typedefaultSet to disabled to turn off the identity governance checks the flow engine makes, and the unlock flows. A recovery flow still completes and releases no lock. Sign-in through the default authentication provider and the Direct API still enforces locks. With disabled, also set account_access.<category>.default.enabled: false for user and agent, and keep it off in the accountAccess section: otherwise locks still form and none can be released early.

Passkey Configuration​

WebAuthn/Passkey settings (typically defined in deployment.yaml).

SettingDescription
passkey.allowed_originsArray of allowed origins for passkey operations

Example:

yaml
passkey:
allowed_origins:
- "https://localhost:8090"

Direct API Configuration​

Controls the Direct API authentication endpoints.

SettingDefaultDescription
direct_api.enabledtrueIf false, ThunderID does not register the Direct API authentication endpoints (/auth/** and /register/passkey/**), and requests to them return 404. AuthZEN access endpoints (/access/**) are not affected

Example:

yaml
direct_api:
enabled: false

To hide the passkey registration endpoints in the Console as well, set direct_api.enabled to false in the Console config.js:

js
direct_api: {
enabled: false,
},

Security Configuration​

Controls server-wide security behavior that is not specific to any single authenticator. Maps to SecurityConfig in the backend, nested under server.security.

SettingDefaultDescription
server.security.jwks_cache_ttl300JWKS cache TTL in seconds. Applies to every JWKS consumer in the server (trusted issuer validation, federated OIDC authenticators such as Google, and so on). Fetched signing keys are reused from the in-process cache for this duration before being re-fetched. Plan external-server key rotations with at least this much overlap. Set to 0 to disable caching
server.security.system_permission_prefix"" (empty)Prefix for system permission strings used in API authorization. When empty, permissions use their base names (for example, system). When set, the prefix is prepended to every system permission (for example, mgmt:system). If you set a prefix, update the Console scopes to match. Changes require a server restart
server.security.direct_auth_secret"" (empty)Secret that gates the Direct API authentication endpoints (/auth/** and /register/passkey/**) and protected AuthZEN access endpoints (/access/**). AuthZEN discovery (/.well-known/authzen-configuration) remains public. The protected endpoints are secure by default. While this is empty they are blocked with 401. When set, callers must send the value in the Direct-Auth-Secret header; a missing or incorrect value is rejected with 401. See Integration Models
server.security.rest.audience"" (unset)Audience (RFC 8707 resource indicator) a self-issued token must carry to be accepted by the REST API gate, normally the identifier of the resource server your management tokens are bound to. While unset the audience is not checked and REST authorizes by scope alone, so this is opt-in. When set, it must be an absolute URI without a fragment; an explicitly empty value fails startup rather than silently disabling the check
server.security.mcp.audience"" (derived)The MCP resource identifier: both the audience an MCP token must carry and the resource value published in the RFC 9728 protected resource metadata. Unlike the REST audience this check is mandatory and cannot be disabled. Leaving it unset derives <server public URL>/mcp rather than turning it off. Set it only when your registered resource server uses a different identifier; it must then match that identifier, or MCP clients following the published metadata will be rejected
server.security.management_api_key_hash"" (empty)SHA-256 hex digest of the management API key that authenticates calls to this deployment's import API (/import, /import/**) and variable store (/variables, /variables/**, /secrets, /secrets/**), so a deployment pipeline can push configuration without first obtaining OAuth client credentials. The key is presented in its own header, API-Key: <key>, never in Authorization. This setting holds the digest, not the key: the deployment only needs to recognize the key, so a leaked configuration file yields nothing a caller can present. Generate the pair with any SHA-256 tool, for example `printf '%s' "$KEY"
server.security.token_revocation.enabledtrueIf true, revoked tokens are rejected on requests to protected APIs. See Token Revocation
server.security.token_revocation.sourcedbWhere the revocation snapshot is read from. db is the only supported value; any other value fails startup validation
server.security.token_revocation.sync_interval_seconds60How often the in-memory revocation snapshot refreshes, in seconds. This sets the normal propagation delay before a revoked token stops being accepted on protected API requests. Token introspection is unaffected and always reads current state
tip

If you set a custom system_permission_prefix, update the Console scopes to match the new permission strings and restart the server.

Trusted Issuer Configuration​

Maps to TrustedIssuerConfig in the backend, nested under server.security.trusted_issuer. Setting server.security.trusted_issuer.issuer activates the feature: ThunderID trusts access tokens issued by an external authorization server and validates them against the external server's JWKS endpoint. When issuer is set, jwks_url and audience are required and ThunderID fails to start if either is missing. This is used for federated authentication scenarios where a central ThunderID instance issues tokens that tenant instances accept.

Enabling a trusted issuer is additive and non-breaking. ThunderID still accepts the tokens it issues itself, routed by the token's iss claim. Existing client_credentials, system, and console callers keep working without configuration changes. See Self-Issued Tokens in the Trusted Issuer guide.

SettingDefaultDescription
server.security.trusted_issuer.issuer""Expected value of the token's iss claim. Must match the external authorization server's issuer URL exactly
server.security.trusted_issuer.jwks_url""URL of the external authorization server's JWKS endpoint used to fetch signing keys. Must use HTTPS (HTTP allowed only for localhost)
server.security.trusted_issuer.audience""Expected value of the token's aud claim. This should be this server's own identifier (typically its public URL)
server.security.trusted_issuer.user_id_claimsubThe claim a caller's id is read from where a control plane records who acted, such as who captured a configuration version or applied one to a gateway (createdBy, appliedBy)
server.security.trusted_issuer.user_lookup.url""The issuer's endpoint for one user, with {id} where the user's id goes, such as https://cloud.example.com/users/{id}. When set, who captured a version or applied one (createdBy, appliedBy) is given out with id and name, the name read from this endpoint when the record is read and never stored. Unset, those fields are left out; the ids are still recorded, so setting it later names the history
server.security.trusted_issuer.user_lookup.token_url""The issuer's token endpoint, asked for a client credentials token for the lookups. Required when url is set
server.security.trusted_issuer.user_lookup.client_id""The machine-to-machine application registered at the issuer for the lookups. Required when url is set
server.security.trusted_issuer.user_lookup.client_secret""That application's secret. Required when url is set
server.security.trusted_issuer.user_lookup.scopes[]The scopes the token is asked for, as the user endpoint requires
server.security.trusted_issuer.user_lookup.name_attributeusernameThe attribute of the user the name is read from
server.security.trusted_issuer.user_lookup.cache_ttl300How long a name once read is kept, in seconds. The token is kept until shortly before it expires
server.security.trusted_issuer.required_claims[]List of claims that every accepted token must contain. Each entry has a claim name and an expected value. If any required claim is missing or does not match, the token is rejected

Example:

yaml
server:
security:
trusted_issuer:
issuer: "https://cloud.example.com"
jwks_url: "https://cloud.example.com/oauth2/jwks"
audience: "https://tenant.example.com"
required_claims:
- claim: "ouId"
value: "tenant-123"
user_lookup:
url: "https://cloud.example.com/users/{id}"
token_url: "https://cloud.example.com/oauth2/token"
client_id: "control-plane-lookup"
client_secret: "<the application's secret>"
scopes: ["internal_user_mgt_view"]
note

The frontend must include a matching resource parameter in the authorization request so the external authorization server sets the token's aud claim to this server's identifier. See the Trusted Issuer guide for the end-to-end setup.

Explore with AI

ThunderID LogoThunderID Logo

Product

DocsAPIsSDKs
© Copyright Linux Foundation Europe.For web site terms of use, trademark policy and other project policies please see https://linuxfoundation.eu/en/policies.