Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Server configuration

The server is configured entirely through environment variables, so the same binary and image run everywhere. Every variable is read once at start-up by api/src/config.rs; .env.example is an annotated template. A .env file in the working directory is loaded first when present, and real environment variables win over it.

Everything that is per tenant (password policy, token lifetimes, MFA, branding, rate-limit ceilings and so on) is not here but in the tenant’s settings; see Tenants and tenant settings and the tenant configuration document.

Conventions used below:

  • An empty or whitespace-only value counts as unset.
  • Booleans accept 1, true, yes, on and 0, false, no, off (any case).
  • Integers are unsigned 32-bit.
  • An invalid or missing required value stops the server with configuration error: ... on standard error and exit code 2.
  • Secrets can come from files. DATABASE_URL, DATABASE_READ_URL, REDIS_URL (and each region’s DATABASE_URL_<NAME>, DATABASE_READ_URL_<NAME>, REDIS_URL_<NAME>), MASTER_KEY, SMTP_PASSWORD, METRICS_TOKEN, AUDIT_SINK_TOKEN, AUDIT_SINK_SECRET and BOOTSTRAP_ADMIN_PASSWORD each also have a *_FILE form (DATABASE_URL_FILE, …) naming a file that holds the value, the convention for Docker and Kubernetes secret mounts. One trailing line ending is dropped and nothing else; an empty file counts as unset, like an empty variable; the variable itself wins when both are set. A file that cannot be read stops the server like an invalid value. The production compose stack passes every secret this way.

Required

VariableTypeMeaning
DATABASE_URLPostgres URLPostgres 16+ connection string. Use a non-superuser role with DML privileges only: superusers bypass row level security, which backs tenant isolation, and a table owner can switch it off. See Postgres and Valkey.
REDIS_URLURLValkey (or Redis) connection: redis://host:6379, rediss:// for TLS, redis+cluster://h1:7000,h2:7001, or redis+sentinel://s1:26379,s2:26379/<master name>. Credentials go before an @ and apply to every host.
PUBLIC_URLhttp/https URLThe externally visible base URL, e.g. https://id.example.com. Every tenant’s issuer is {PUBLIC_URL}/t/{slug} (a tenant with a custom domain uses https://{domain} instead). Changing it changes every issuer.
MASTER_KEY or MASTER_KEY_FILE32 bytesThe master key that encrypts secrets at rest (signing keys, identity providers’ client secrets, SMTP and SMS credentials, webhook secrets, second-factor secrets). MASTER_KEY is hex or base64 (standard or URL-safe); MASTER_KEY_FILE names a file holding the raw 32 bytes or the same text encodings. MASTER_KEY wins when both are set. Generate with openssl rand -hex 32. See Signing keys and the master key. Optional when KEY_WRAPPER is set.

Server

VariableTypeDefaultMeaning
BIND_ADDRsocket address0.0.0.0:8080Address the HTTP (or HTTPS) listener binds to. ridm-api --healthcheck probes /healthz at this address (a wildcard 0.0.0.0 or :: becomes the loopback address of the same family).
UI_URLURLPUBLIC_URLBase URL of the sign-in, consent and console pages. The server redirects browsers to pages under it (/login/, /consent/, /console/, …) and derives the built-in console clients’ redirect URIs from it, updating them at start-up when it changes. Leave it unset when the server serves its embedded UI; set it to wherever the UI is hosted otherwise.
EMBEDDED_UIbooleantrueServe the UI compiled into the binary (the container image’s build, --features embedded-ui) on PUBLIC_URL’s origin. Has no effect on a binary built without the feature, or when UI_URL is another origin, and false turns the pages off on a node that should answer the API alone. See Deployment overview.
DOCS_ENABLEDbooleanfalseServe Swagger UI at /docs. /openapi.json is served either way. Keep it off in production.
MIGRATE_ON_STARTbooleanfalseAt start-up, apply migrations if any are pending, as DATABASE_URL’s role; with none pending nothing is applied, so the DML-only app role can run with it on once ridm-api migrate has brought the schema up to date. A pending migration then fails start-up unless the role owns the schema. Off, the server only logs a warning when migrations are pending. ridm-api bootstrap follows the same setting. See Container image.

Database and cache

VariableTypeDefaultMeaning
DATABASE_READ_URLPostgres URLunsetA read replica for listings and statistics, run in read-only transactions. Anything that feeds a decision stays on the primary. Unset, the primary serves both.
DB_POOL_MINinteger2Minimum Postgres connections per node. Must not exceed DB_POOL_MAX.
DB_POOL_MAXinteger20Maximum Postgres connections per node. Roughly twice the database server’s CPU count divided by the number of API nodes; every node’s pools together must stay under the server’s max_connections.
DB_ACQUIRE_TIMEOUT_MSinteger ≥ 12000How long a request waits for a free pooled connection before failing with 503. An overloaded pool fails fast instead of queueing requests for seconds; raise DB_POOL_MAX (or add nodes) if it happens under normal load.
REDIS_POOL_MAXinteger32Valkey connections per node (at least 1).
REDIS_TLS_CA_FILEpathunsetPEM roots to verify a rediss:// server against, instead of the system’s. Single-server form only.
REDIS_TLS_CERT_FILE, REDIS_TLS_KEY_FILEpathsunsetA client certificate (chain) and its key, PEM, presented to a rediss:// server; both or neither. With Valkey’s tls-auth-clients yes and tls-auth-clients-user CN the connection is authenticated as the user the certificate’s CN names, with no password. The three files are re-read whenever they change, so a renewal needs no restart. Per region: REDIS_TLS_CA_FILE_<NAME>, REDIS_TLS_CERT_FILE_<NAME>, REDIS_TLS_KEY_FILE_<NAME>. See Postgres and Valkey.
DATA_REGIONSregion listunsetRegional databases tenants can be placed in, comma-separated names (1-32 lowercase letters, digits or hyphens, starting with a letter; home is the DATABASE_URL database). Each region <name> needs DATABASE_URL_<NAME> (upper case, - as _: eu-west → DATABASE_URL_EU_WEST) and may set DATABASE_READ_URL_<NAME> and REDIS_URL_<NAME>; all three also have a *_FILE form. Every node must list the same regions. See Data residency.

Security and keys

VariableTypeDefaultMeaning
MASTER_KEY_VERSIONinteger ≥ 11Generation number of the current master key, stored with every ciphertext. Bump it when rotating the master key.
MASTER_KEY_PREVIOUSversion=key listunsetOlder generations still needed to decrypt rows not yet re-encrypted: 1=<hex>,2=<hex>. Every version must be lower than MASTER_KEY_VERSION. Remove an entry once ridm-api rotate-master-key --status (or GET /admin/master-key) shows nothing left under it.
KEY_WRAPPERpkcs11, aws-kms, vault, gcp-kms or azure-key-vaultunsetAn HSM or KMS holds the master-key generations instead of the environment; each backend has settings of its own. See Key custody: HSM and KMS.
KEY_WRAPPER_PREVIOUSbackend listunsetFurther backends still holding older generations, comma-separated, while rows move off them.
COOKIE_SECUREbooleantrueSet the Secure attribute on the session and trusted-device cookies and give them the __Host- prefix (__Host-ridm_session_{slug}, __Host-ridm_device_{slug}; without it ridm_session_{slug}, ridm_device_{slug}). Only turn off for plain-HTTP local development. See TLS and reverse proxies.
TRUSTED_PROXIESCIDR/IP listemptyComma-separated networks whose X-Forwarded-For, Forwarded and X-Forwarded-Host headers are believed. The client address is the TCP peer unless the peer is in this list. Empty means headers are never trusted. See TLS and reverse proxies.
OUTBOUND_ALLOW_NETWORKSCIDR/IP listemptyComma-separated private networks that requests to tenant-chosen URLs (webhooks, back-channel logout, client jwks_uri, identity provider endpoints, tenant email/SMS gateways and SMTP hosts, CAPTCHA) may reach, for applications on an internal network. Addresses inside them count as public, whether given literally or resolved. Empty means public addresses only (plus localhost for development). See the outbound request policy.
HSTS_MAX_AGEinteger (seconds)63072000 (two years)Strict-Transport-Security max-age, sent only when PUBLIC_URL is https. 0 disables the header.
SECURITY_CONTACTURI listunsetComma-separated mailto:, tel: or https: URIs, written as the Contact fields of /.well-known/security.txt (RFC 9116), with an Expires 30 days ahead, rolling. Unset (with SECURITY_TXT unset too), that path answers 404: the document names whoever handles vulnerability reports for this deployment, which only its operator can say.
SECURITY_POLICY_URLhttps URLunsetWritten as the document’s Policy field. Needs SECURITY_CONTACT.
SECURITY_TXT or SECURITY_TXT_FILEtextunsetThe whole document, served byte for byte, for an operator who writes (or OpenPGP-signs) their own. It must hold at least one Contact and exactly one RFC 3339 Expires; the server warns at start-up once that date has passed, and keeps serving it. Cannot be combined with the two above.
ARGON2_M_COST_KIBinteger19456 (19 MiB)argon2id memory cost for password hashes.
ARGON2_T_COSTinteger2argon2id iterations.
ARGON2_P_COSTinteger1argon2id lanes.
BREACH_CHECK_URLURL, or off/none/falsehttps://api.pwnedpasswords.com/range/Have I Been Pwned compatible range endpoint used by tenants with password.check_breached on (k-anonymity: only a five-character hash prefix leaves the server). A trailing / is added when missing. off, none or false disables the check for the whole deployment (air-gapped installs).

Geo-IP

Where the country and coordinates behind adaptive authentication’s location signals come from. Both sources are optional and consulted in this order; with neither, a tenant’s risk policy still scores the device and velocity signals and simply never raises a location one.

VariableTypeDefaultMeaning
GEOIP_COUNTRY_HEADERSheader name listcloudfront-viewer-country,cf-ipcountry,x-geo-countryComma-separated headers a proxy or CDN sets, first one present wins. An ISO 3166-1 alpha-2 code; XX and T1 mean “unknown” and are ignored. Only read when the request came through a TRUSTED_PROXIES peer, exactly like X-Forwarded-For.
GEOIP_LATITUDE_HEADERSheader name listcloudfront-viewer-latitude,x-geo-latitudeCoordinates from the same proxy, needed for impossible travel. A country without them still works.
GEOIP_LONGITUDE_HEADERSheader name listcloudfront-viewer-longitude,x-geo-longitude
GEOIP_DBfile pathunsetA MaxMind DB file (GeoLite2 or GeoIP2, City or Country), read into memory at start-up and used when no trusted header answered. rIDM bundles no data: supply and refresh the file yourself, under its own licence. A file that cannot be read is logged and treated as absent.

The argon2 parameters may not go below 8 MiB, 1 iteration and 1 lane. Hashes made with weaker parameters are upgraded on the user’s next successful sign-in, so raising them is safe.

Native TLS

VariableTypeDefaultMeaning
TLS_CERTfile pathunsetPEM certificate chain.
TLS_KEYfile pathunsetPEM private key.

Both or neither: setting one alone is a configuration error. With both, the listener speaks HTTPS (rustls); leave them unset when a reverse proxy or load balancer terminates TLS. ridm-api --healthcheck then probes over HTTPS, trusting exactly the first certificate in TLS_CERT.

Mutual TLS

Client certificates for mutual-TLS client authentication and certificate-bound tokens (RFC 8705). Setting MTLS_BIND or CLIENT_CERT_HEADER turns the feature on: discovery then offers tls_client_auth and self_signed_tls_client_auth and tls_client_certificate_bound_access_tokens. Without either, a client registered for one of them cannot authenticate.

VariableTypeDefaultMeaning
MTLS_BINDsocket addressunsetA second listener (0.0.0.0:8443) that serves the same routes and asks every connection for a client certificate without requiring one. The handshake proves the client holds the certificate’s key; which certificates are accepted is decided per client.
MTLS_CERT / MTLS_KEYfile pathTLS_CERT / TLS_KEYThat listener’s server certificate and key (PEM), both or neither. MTLS_BIND without them or TLS_CERT/TLS_KEY is a configuration error.
MTLS_PUBLIC_URLURLunsetWhere clients reach the mTLS endpoints (https://mtls.id.example.com). Discovery publishes {MTLS_PUBLIC_URL}/t/{slug}/token and the other client-authenticated endpoints as mtls_endpoint_aliases, so browsers never meet a certificate prompt on the sign-in host. Needs MTLS_BIND or CLIENT_CERT_HEADER.
CLIENT_CERT_HEADERheader nameunsetThe header a TLS-terminating proxy puts the client certificate in (X-Client-Cert): PEM, URL-encoded PEM (nginx $ssl_client_escaped_cert) or base64 DER, leaf first, up to five certificates. Only read from a TRUSTED_PROXIES peer; the proxy must overwrite it on every request.

FIPS build

Read only by the FIPS build (--features fips, the -fips image). The standard build ignores them.

VariableTypeDefaultMeaning
FIPS_TRANSITIONbooleanfalseAllows the one-time reads FIPS doesn’t approve while a deployment moves onto the FIPS build: secrets at rest still sealed with XChaCha20-Poly1305, and password hashes other than PBKDF2 (argon2, bcrypt, MD5, salted digests), at sign-in and in bulk imports. Each read is logged as a warning and counted in ridm_fips_non_approved_total. Turn it off once rotate-master-key has moved everything (see FIPS 140-3).
PBKDF2_ITERATIONSinteger210000PBKDF2-HMAC-SHA512 iterations for new password hashes, 210,000 (OWASP’s floor for SHA-512) to 1,000,000 (the most a stored hash may ask for). Raising it re-hashes each user at their next sign-in. The standard build hashes with argon2id (ARGON2_*) instead.
FIPS_ALLOW_NON_FIPS_HOSTbooleanfalseLets the FIPS build start on a host that isn’t in FIPS mode (/proc/sys/crypto/fips_enabled is not 1). It prints and logs a warning. The AWS-LC module self-test must still pass. For development and CI only: such a host isn’t a FIPS-validated configuration.

Email

Deployment-wide SMTP defaults, used by tenants that have not configured their own email backend (PUT /admin/tenants/{slug}/messaging/email). There is no deployment-wide SMS gateway; SMS is configured per tenant. See Email, SMS and templates.

VariableTypeDefaultMeaning
SMTP_HOSThost nameunsetSetting it turns the defaults on. Chosen by the operator, so it may be a private relay; tenant SMTP hosts are held to the outbound request policy instead.
SMTP_PORTinteger587
SMTP_USERNAMEstringunset
SMTP_PASSWORDstringunset
SMTP_FROMmailboxrequired when SMTP_HOST is setFrom header, e.g. "rIDM <no-reply@example.com>".
SMTP_SECURITYstarttls, tls, nonestarttlstls is implicit TLS (usually port 465); none is plain SMTP, for a local catcher such as Mailpit.

Rate limits

Per-tenant ceilings live in settings.rate_limits (see Rate limits, IP rules and CAPTCHA). These two belong to the deployment:

VariableTypeDefaultMeaning
RATE_LIMITSbooleantrueMaster switch for every request ceiling. Off only for tests and local experiments. Tenant IP rules are enforced either way.
RATE_LIMIT_IP_PER_MINUTEinteger6000Requests per minute one client address may make to every limited endpoint of every tenant together. 0 turns this bucket off.

Logging, metrics, tracing and audit export

VariableTypeDefaultMeaning
LOG_FORMATjson, pretty (or text)jsonLog line format. JSON lines carry the current span’s fields.
RUST_LOGtracing filterinfoLog filter, e.g. info,ridm_api=debug,sqlx=warn.
METRICS_TOKENstringunsetWhen set, GET /metrics demands Authorization: Bearer <token>; unset, it is open.
OTEL_EXPORTER_OTLP_ENDPOINTURLunsetOTLP/HTTP collector base URL (e.g. http://otel-collector:4318); traces are posted to <url>/v1/traces as protobuf: one info span per request, named METHOD /route/{template}, plus whatever else RUST_LOG enables. Unset, no exporter runs.
OTEL_SERVICE_NAMEstringridmservice.name on exported traces.
AUDIT_SINK_URLURLunsetAlso ship every audit row, from the database, so nothing is lost while the receiver is down: https://... (JSON arrays of up to 100 rows), syslog://host:514 / syslog+udp://host:514 (UDP), syslog+tcp://host:514 (RFC 5424, one message per line) or syslog+tls://host:6514 (RFC 5425). See Shipping to an external system.
AUDIT_SINK_TOKENstringunsetSent as Authorization: Bearer <token> to an HTTP(S) audit sink.
AUDIT_SINK_SECRETstringunsetSigns each HTTP(S) batch: X-RIDM-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "<t>.<body>">.
AUDIT_SINK_CA_FILEpathunsetPEM certificates to trust for the sink (HTTPS or syslog+tls) instead of the system’s roots.
HOSTNAMEstring-The host field of syslog audit lines. Usually set by the container runtime.

See Observability for the metric names and the audit sink’s delivery behaviour.

Background jobs

VariableTypeDefaultMeaning
RETENTION_DAYSinteger ≥ 130Days the hourly cleanup job keeps spent rows: expired, revoked or consumed refresh tokens, ended sessions (expired, idle past their idle timeout, or revoked; at most a week), login attempts, sent or dead messages, delivered or dead webhook deliveries, expired or used invitations, expired or revoked trusted devices, personal access tokens and provisioning tokens.
EVENT_QUEUE_CAPACITYinteger ≥ 1000100000Events each in-process consumer (the audit writer, the webhook dispatcher) may have waiting on a node. /readyz reports events: saturated once a queue is 80% full, so a load balancer stops sending the node traffic while it catches up; an event published into a full queue is dropped, logged and counted (ridm_event_queue_dropped_total). Each queued event holds roughly a kilobyte.

Audit retention is per tenant (settings.audit.retention_days), not governed by this variable.

First-run bootstrap

A development convenience: at start-up, create the master tenant’s first global owner from the environment. It is a no-op once a user in master holds the owner role. Production installs usually run ridm bootstrap or ridm-api bootstrap once instead; both fall back to these variables for values not given as flags.

VariableTypeDefaultMeaning
BOOTSTRAP_ADMIN_EMAILemailunsetEmail of the first owner.
BOOTSTRAP_ADMIN_PASSWORDstringunsetInitial password; the account must change it at first sign-in.
BOOTSTRAP_ADMIN_USERNAMEstringadminUsername of the first owner.
BOOTSTRAP_SAMPLE_CLIENTbooleanfalseAlso make sure master has the public single-page-app client sample-spa: PKCE, redirect URI http://localhost:3000/callback, post-logout redirect http://localhost:3000/, CORS origin http://localhost:3000. Created once; an existing one is left alone. Read only when the two variables above are set, at start-up and by ridm-api bootstrap.

BOOTSTRAP_ADMIN_EMAIL and BOOTSTRAP_ADMIN_PASSWORD must be set together or not at all.

Server subcommands

The ridm-api binary runs the server by default and has these subcommands. All except openapi and --healthcheck load the full configuration above; migrate leaves out the master key and key custody settings, which migrations never need.

CommandPurpose
ridm-api migrateApply pending migrations, then exit. Run it as the schema-owner role.
ridm-api bootstrap [--email E] [--username U] [--password-stdin] [--no-must-change]Create the first global administrator. Applies pending migrations only with MIGRATE_ON_START=true; otherwise pending migrations make it exit 1 and name ridm-api migrate.
ridm-api rotate-master-key [--status | --new-generation]Re-encrypt secrets at rest under the current generation, or report what is left. --new-generation first has the key custody backend wrap a new generation and makes it current.
ridm-api openapiPrint the admin API’s OpenAPI document; needs no database.
ridm-api --healthcheckExit 0 when /healthz answers at BIND_ADDR (over HTTPS when TLS_CERT is set), 1 otherwise; the image’s HEALTHCHECK runs it.

Command-line tool (ridm)

The ridm CLI reads these variables; each overrides the stored profile, and the matching flag overrides the variable. It also loads a .env file from the working directory.

VariableFlagMeaning
RIDM_URL--urlServer origin, e.g. https://id.example.com.
RIDM_TOKEN--tokenBearer token to present (an access token or a personal access token). Never written to disk.
RIDM_TENANT--tenantTenant slug to act on (master by default).
RIDM_PROFILE--profileStored profile to use.
RIDM_CONFIGPath of the profile file. Default $XDG_CONFIG_HOME/ridm/config.json, else ~/.config/ridm/config.json (created with mode 0600).

ridm bootstrap talks to the database directly and so reads the server’s own DATABASE_URL, REDIS_URL and MASTER_KEY or key custody settings (and the BOOTSTRAP_* variables).

UI build and development

The Next.js UI in ui/ is a static export; these are read when it is built or run with next dev, not by the server.

VariableWhenMeaning
NEXT_PUBLIC_API_URLbuild timeBase URL of the API when the UI is hosted on another origin. Empty (the default) means same origin.
API_PROXYnext dev onlyProxy /t, /admin, /healthz, /readyz, /.well-known and /openapi.json to this API origin, so the pages call it same-origin, e.g. API_PROXY=http://localhost:8080 npm run dev.