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,onand0,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 code2. - Secrets can come from files.
DATABASE_URL,DATABASE_READ_URL,REDIS_URL(and each region’sDATABASE_URL_<NAME>,DATABASE_READ_URL_<NAME>,REDIS_URL_<NAME>),MASTER_KEY,SMTP_PASSWORD,METRICS_TOKEN,AUDIT_SINK_TOKEN,AUDIT_SINK_SECRETandBOOTSTRAP_ADMIN_PASSWORDeach also have a*_FILEform (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
| Variable | Type | Meaning |
|---|---|---|
DATABASE_URL | Postgres URL | Postgres 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_URL | URL | Valkey (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_URL | http/https URL | The 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_FILE | 32 bytes | The 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
| Variable | Type | Default | Meaning |
|---|---|---|---|
BIND_ADDR | socket address | 0.0.0.0:8080 | Address 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_URL | URL | PUBLIC_URL | Base 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_UI | boolean | true | Serve 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_ENABLED | boolean | false | Serve Swagger UI at /docs. /openapi.json is served either way. Keep it off in production. |
MIGRATE_ON_START | boolean | false | At 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
| Variable | Type | Default | Meaning |
|---|---|---|---|
DATABASE_READ_URL | Postgres URL | unset | A 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_MIN | integer | 2 | Minimum Postgres connections per node. Must not exceed DB_POOL_MAX. |
DB_POOL_MAX | integer | 20 | Maximum 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_MS | integer ≥ 1 | 2000 | How 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_MAX | integer | 32 | Valkey connections per node (at least 1). |
REDIS_TLS_CA_FILE | path | unset | PEM roots to verify a rediss:// server against, instead of the system’s. Single-server form only. |
REDIS_TLS_CERT_FILE, REDIS_TLS_KEY_FILE | paths | unset | A 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_REGIONS | region list | unset | Regional 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
| Variable | Type | Default | Meaning |
|---|---|---|---|
MASTER_KEY_VERSION | integer ≥ 1 | 1 | Generation number of the current master key, stored with every ciphertext. Bump it when rotating the master key. |
MASTER_KEY_PREVIOUS | version=key list | unset | Older 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_WRAPPER | pkcs11, aws-kms, vault, gcp-kms or azure-key-vault | unset | An 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_PREVIOUS | backend list | unset | Further backends still holding older generations, comma-separated, while rows move off them. |
COOKIE_SECURE | boolean | true | Set 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_PROXIES | CIDR/IP list | empty | Comma-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_NETWORKS | CIDR/IP list | empty | Comma-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_AGE | integer (seconds) | 63072000 (two years) | Strict-Transport-Security max-age, sent only when PUBLIC_URL is https. 0 disables the header. |
SECURITY_CONTACT | URI list | unset | Comma-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_URL | https URL | unset | Written as the document’s Policy field. Needs SECURITY_CONTACT. |
SECURITY_TXT or SECURITY_TXT_FILE | text | unset | The 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_KIB | integer | 19456 (19 MiB) | argon2id memory cost for password hashes. |
ARGON2_T_COST | integer | 2 | argon2id iterations. |
ARGON2_P_COST | integer | 1 | argon2id lanes. |
BREACH_CHECK_URL | URL, or off/none/false | https://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.
| Variable | Type | Default | Meaning |
|---|---|---|---|
GEOIP_COUNTRY_HEADERS | header name list | cloudfront-viewer-country,cf-ipcountry,x-geo-country | Comma-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_HEADERS | header name list | cloudfront-viewer-latitude,x-geo-latitude | Coordinates from the same proxy, needed for impossible travel. A country without them still works. |
GEOIP_LONGITUDE_HEADERS | header name list | cloudfront-viewer-longitude,x-geo-longitude | |
GEOIP_DB | file path | unset | A 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
| Variable | Type | Default | Meaning |
|---|---|---|---|
TLS_CERT | file path | unset | PEM certificate chain. |
TLS_KEY | file path | unset | PEM 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.
| Variable | Type | Default | Meaning |
|---|---|---|---|
MTLS_BIND | socket address | unset | A 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_KEY | file path | TLS_CERT / TLS_KEY | That 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_URL | URL | unset | Where 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_HEADER | header name | unset | The 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.
| Variable | Type | Default | Meaning |
|---|---|---|---|
FIPS_TRANSITION | boolean | false | Allows 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_ITERATIONS | integer | 210000 | PBKDF2-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_HOST | boolean | false | Lets 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. |
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.
| Variable | Type | Default | Meaning |
|---|---|---|---|
SMTP_HOST | host name | unset | Setting 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_PORT | integer | 587 | |
SMTP_USERNAME | string | unset | |
SMTP_PASSWORD | string | unset | |
SMTP_FROM | mailbox | required when SMTP_HOST is set | From header, e.g. "rIDM <no-reply@example.com>". |
SMTP_SECURITY | starttls, tls, none | starttls | tls 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:
| Variable | Type | Default | Meaning |
|---|---|---|---|
RATE_LIMITS | boolean | true | Master switch for every request ceiling. Off only for tests and local experiments. Tenant IP rules are enforced either way. |
RATE_LIMIT_IP_PER_MINUTE | integer | 6000 | Requests 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
| Variable | Type | Default | Meaning |
|---|---|---|---|
LOG_FORMAT | json, pretty (or text) | json | Log line format. JSON lines carry the current span’s fields. |
RUST_LOG | tracing filter | info | Log filter, e.g. info,ridm_api=debug,sqlx=warn. |
METRICS_TOKEN | string | unset | When set, GET /metrics demands Authorization: Bearer <token>; unset, it is open. |
OTEL_EXPORTER_OTLP_ENDPOINT | URL | unset | OTLP/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_NAME | string | ridm | service.name on exported traces. |
AUDIT_SINK_URL | URL | unset | Also 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_TOKEN | string | unset | Sent as Authorization: Bearer <token> to an HTTP(S) audit sink. |
AUDIT_SINK_SECRET | string | unset | Signs each HTTP(S) batch: X-RIDM-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "<t>.<body>">. |
AUDIT_SINK_CA_FILE | path | unset | PEM certificates to trust for the sink (HTTPS or syslog+tls) instead of the system’s roots. |
HOSTNAME | string | - | 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
| Variable | Type | Default | Meaning |
|---|---|---|---|
RETENTION_DAYS | integer ≥ 1 | 30 | Days 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_CAPACITY | integer ≥ 1000 | 100000 | Events 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.
| Variable | Type | Default | Meaning |
|---|---|---|---|
BOOTSTRAP_ADMIN_EMAIL | unset | Email of the first owner. | |
BOOTSTRAP_ADMIN_PASSWORD | string | unset | Initial password; the account must change it at first sign-in. |
BOOTSTRAP_ADMIN_USERNAME | string | admin | Username of the first owner. |
BOOTSTRAP_SAMPLE_CLIENT | boolean | false | Also 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.
| Command | Purpose |
|---|---|
ridm-api migrate | Apply 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 openapi | Print the admin API’s OpenAPI document; needs no database. |
ridm-api --healthcheck | Exit 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.
| Variable | Flag | Meaning |
|---|---|---|
RIDM_URL | --url | Server origin, e.g. https://id.example.com. |
RIDM_TOKEN | --token | Bearer token to present (an access token or a personal access token). Never written to disk. |
RIDM_TENANT | --tenant | Tenant slug to act on (master by default). |
RIDM_PROFILE | --profile | Stored profile to use. |
RIDM_CONFIG | Path 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.
| Variable | When | Meaning |
|---|---|---|
NEXT_PUBLIC_API_URL | build time | Base URL of the API when the UI is hosted on another origin. Empty (the default) means same origin. |
API_PROXY | next dev only | Proxy /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. |