# Configuration The server is configured from `D_OAUTH_*` environment variables plus an optional file named by `D_OAUTH_CONFIG`. Configuration is validated at load time and the process refuses to start on an insecure or inconsistent setting. ## Purpose This guide lists every configuration key, its default, and its validation rule, so an operator can build a correct environment without reading the source. The loader lives in `src/config.rs` and its submodules. ## How loading works `Config::load` builds a layered source: 1. An environment source with prefix `D_OAUTH`, prefix separator `_`, and nesting separator `__`, lowercased and parsed with type coercion. 2. An optional file from `D_OAUTH_CONFIG` (YAML or TOML by extension). It is not required. 3. Two conventional secret-file overrides: `DATABASE_PASSWORD_FILE` sets `database.password_file`, and `REDIS_PASSWORD_FILE` sets `redis.password_file`. The environment spelling of a nested key is `D_OAUTH_` followed by the path with every `.` replaced by `__`, uppercased. For example: | YAML path | Environment variable | | --- | --- | | `database.url` | `D_OAUTH_DATABASE__URL` | | `redis.connect_timeout_secs` | `D_OAUTH_REDIS__CONNECT_TIMEOUT_SECS` | | `ttl.access_secs` | `D_OAUTH_TTL__ACCESS_SECS` | | `failure_policy.request_timeout_secs` | `D_OAUTH_FAILURE_POLICY__REQUEST_TIMEOUT_SECS` | | `tls.cert_path` | `D_OAUTH_TLS__CERT_PATH` | A secret may be given inline or as a mounted file. Inline wins when both are set. Mounted files are read and trimmed of trailing newlines. The key ring follows the same pattern: `token_pepper_file` and `totp_encryption_key_file` (environment spellings `D_OAUTH_TOKEN_PEPPER_FILE` and `D_OAUTH_TOTP_ENCRYPTION_KEY_FILE`) name mounted files. The cluster Deployment uses those file forms so the material never enters the process environment; mount them `0400` on a tmpfs (the shipped manifests project them at `/run/secrets` with group `65532`). The inline forms are retained and win when both are set, so an existing environment-based deployment still starts. ## Top-level keys | Key | Default | Rule | | --- | --- | --- | | `issuer` | required | Absolute `https` URL with no trailing slash and no whitespace in the authority. | | `bind_addr` | `0.0.0.0:8080` | A socket address. The TLS public listener. | | `health_addr` | `0.0.0.0:8081` | A socket address. `serve` refuses a loopback address; `config check` does not. The plaintext health listener. | | `environment` | `production` | `production` or `development`. Development relaxes the database TLS requirement. | | `cookie_domain` | none (host-only) | Optional cookie domain. | | `cookie_secure` | `true` | Adds the `Secure` attribute. Set `false` only for localhost testing. | | `trusted_proxy` | none | An IP. When set, that proxy's forwarded client-cert and client-IP headers are honored. When unset, those headers are stripped. | | `allow_open_registration` | `false` | When false, DCR requires a valid initial-access token. | | `dcr_redirect_policy` | `catalog` | `catalog`, `catalog_loopback`, or `open`. See below. | | `cors_allowed_origins` | empty | An explicit origin allowlist. Empty means no CORS layer. A lone `*` is silently dropped (no CORS layer, no error). | | `allow_token_exchange` | `false` | Enables the token-exchange grant. | | `token_exchange_allowlist` | empty | Rules of `{caller, subject, scopes[]}` for token exchange. | ## TLS keys | Key | Default | Rule | | --- | --- | --- | | `tls.cert_path` | required | PEM certificate chain for the public listener. | | `tls.key_path` | required | PEM private key matching the chain. | | `tls.client_ca_path` | none | Optional client-CA bundle for in-process mTLS peer verification. | ## Database keys Production requires `sslmode=verify-full` in `database.url` unless `require_verify_full` is set to false. | Key | Default | | --- | --- | | `database.url` | required | | `database.password_file` | none | | `database.root_cert_path` | none (`sslrootcert`) | | `database.max_connections` | `15` | | `database.min_connections` | `2` | | `database.acquire_timeout_secs` | `3` | | `database.idle_timeout_secs` | `300` | | `database.max_lifetime_secs` | `1800` | | `database.statement_timeout_secs` | `3` | | `database.lock_timeout_secs` | `1` | | `database.idle_in_transaction_timeout_secs` | `30` | | `database.application_name` | `oauth21-server` | | `database.require_verify_full` | `true` | `max_connections` must be greater than zero and at least `min_connections`. ## Redis keys Exactly one topology is selected: `url` for single node, `nodes` plus `master_name` for Sentinel, or `seeds` for Cluster. Setting more than one, or none, is rejected as ambiguous. | Key | Default | Notes | | --- | --- | --- | | `redis.url` | none | Single-node `redis://` or `rediss://` URL. | | `redis.nodes` | `[]` | Sentinel `host:port` addresses. | | `redis.master_name` | none | Required for Sentinel. | | `redis.seeds` | `[]` | Cluster seed `host:port` addresses. | | `redis.sentinel_auth` | none | Sentinel node password. | | `redis.data_auth` | none | Sentinel data-node password. | | `redis.auth` | none | Cluster password. | | `redis.tls` | `false` | TLS for Sentinel and Cluster connections. | | `redis.connect_timeout_secs` | `3` | Connection timeout. | | `redis.response_timeout_secs` | `2` | Per-command response timeout. | | `redis.retry_budget` | `6` | Reconnect attempts. | | `redis.retry_min_delay_ms` | `100` | Minimum backoff. | | `redis.retry_max_delay_ms` | `5000` | Maximum backoff. | | `redis.durability` | required | `wait`, `waitaof`, or `appendfsync_always`. See [REDIS.md](REDIS.md). | | `redis.noeviction_required` | `true` | Requires `maxmemory-policy noeviction` at startup. | | `redis.aof_required` | `true` | Requires `appendonly yes` at startup. | | `redis.token_key_prefix` | `oauth:{state}:` | Must contain the `{state}` hash tag. | | `redis.ratelimit_key_prefix` | `oauth:{rl}:` | Must contain the `{rl}` hash tag and differ from the token prefix. | | `redis.root_cert_path` | none | Root certificate for `rediss://` endpoints. | | `redis.password_file` | none | Mounted password for the data node. | Durability must match the topology. Single node requires `waitaof` or `appendfsync_always`; replicated topologies require `wait`. A mismatch is rejected. ## Key-ring keys | Key | Default | Notes | | --- | --- | --- | | `token_pepper` | none | Inline token-pepper secret. Required unless `token_pepper_file` is set. | | `token_pepper_file` | none | Mounted token-pepper secret. | | `token_pepper_id` | `v1` | Version id stored with the hash so peppers can rotate. | | `totp_encryption_key` | none | Inline AES-256-GCM key for TOTP secrets. Required to enroll TOTP. | | `totp_encryption_key_file` | none | Mounted TOTP key. | | `totp_key_id` | `v1` | Version id carried in the encrypted TOTP blob. | The token pepper is required: without it the key ring cannot be built and the server refuses to start. The shipped cluster manifests set `D_OAUTH_TOKEN_PEPPER_FILE` and `D_OAUTH_TOTP_ENCRYPTION_KEY_FILE` rather than the inline keys; see [DEPLOYMENT.md](DEPLOYMENT.md). ## Failure policy keys Security endpoints are always fail-closed. Setting `open` on any of them is a load-time error. | Key | Default | | --- | --- | | `failure_policy.default_mode` | `closed` | | `failure_policy.endpoints` | the seven security endpoints mapped to `closed` | | `failure_policy.retryable` | `io`, `timeout`, `connection_reset`, `temporarily_unavailable`, `pool_timed_out` | | `failure_policy.retry_budget` | `3` | | `failure_policy.request_timeout_secs` | `10` | The security endpoints are `authorize`, `token`, `introspect`, `revoke`, `par`, `device_authorization`, and `register`. ## Lifetime and hashing keys | Key | Default | | --- | --- | | `ttl.access_secs` | `300` | | `ttl.refresh_secs` | `2592000` (30 days) | | `ttl.code_secs` | `60` | | `ttl.par_secs` | `90` | | `ttl.device_secs` | `600` | | `ttl.dcr_initial_access_secs` | `86400` (24 hours) | | `ttl.session_secs` | `43200` (12 hours) | | `argon2.memory_kib` | `19456` (19 MiB) | | `argon2.iterations` | `2` | | `argon2.parallelism` | `1` | Every TTL must be non-zero. Argon2 memory must be at least 8192 KiB, with a non-zero iteration count and parallelism. ## Rate-limit keys | Key | Default | | --- | --- | | `rate_limit.enabled` | `true` | | `rate_limit.window_secs` | `60` | | `rate_limit.per_ip` | `120` | | `rate_limit.per_client` | `300` | | `rate_limit.login_per_ip` | `20` | | `rate_limit.token_per_client` | `60` | The window must be non-zero. ## Scope registry and token exchange `scope_registry` is an optional declarative input applied only by `config sync`. It is never required to be non-empty. Each entry is `{name, description?, allowed_grants?, allowed_client_classes?}`. The name must match the RFC 6749 scope-token grammar, may not be `openid`, and may not exceed 128 characters. `token_exchange_allowlist` entries are `{caller, subject, scopes[]}`; caller and subject may not be empty. ## The timeout ladder The four database and request timeouts must satisfy: ``` lock_timeout < statement_timeout <= acquire_timeout < request_timeout ``` With the defaults that is `1s < 3s <= 3s < 10s`. A violation is a load-time error, which prevents a request from outliving a pool slot or a lock outliving the statement. ## dcr_redirect_policy - `catalog`: only redirect URIs present in the runtime catalog may be registered. This is the default. - `catalog_loopback`: catalog URIs plus dynamic loopback URIs (RFC 8252). - `open`: any redirect URI may be registered. This is an explicit opt-in and is not the default. ## Security properties - No secret is ever printed. `Config`'s `Debug` impl redacts the database URL, Redis URLs, and every password. - A production database URL without `sslmode=verify-full` is refused. - A Redis topology that does not satisfy the durability contract is refused at load and re-checked live at startup. - `trusted_proxy` is unset by default, so forwarded client-cert headers from an untrusted hop are stripped before any handler reads them. - CORS and open registration are off by default. - File-backed pepper/TOTP keys are read from a `0400` tmpfs file, never from the process environment. ## Edge cases - `D_OAUTH_CONFIG` pointing at a missing file is not an error; the file is optional. - An inline `token_pepper` of only whitespace is treated as absent. - An unreadable `*_file` path is a load-time error (`ConfigError::SecretFile`), never a silent fallback to an absent key. - `scope delete` and `redirect-uri delete` disable a referenced row instead of deleting it. See [OPERATIONS.md](OPERATIONS.md). - A deliberate mismatch of `dcr_redirect_policy = open` and mTLS is a deployment error, not a load error: open registration lets a caller register an arbitrary redirect URI. ## Operational runbook Validate any environment before starting the server: ```bash D_OAUTH_ISSUER=https://auth.example.com cargo run --bin d-oauth-server -- config check ``` `config check` prints `config ok: issuer=...` and exits 0 on success, or prints the first validation error and exits 1. It does not connect to Postgres or Redis. ## File and directory references - `src/config.rs` for the loader and `ConfigError`. - `src/config/build.rs` for defaults and cross-field validation. - `src/config/db.rs`, `src/config/redis.rs`, `src/config/types.rs`, `src/config/policy.rs`, `src/config/scope.rs` for the per-area keys. - `src/config/tests.rs` for the asserted defaults.