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.
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.
Config::load builds a layered source:
D_OAUTH, prefix separator _, and
nesting separator __, lowercased and parsed with type coercion.D_OAUTH_CONFIG (YAML or TOML by extension). It is
not required.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.
| 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. |
| 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. |
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.
database.root_cert_path (or sslrootcert in the URL) is required in practice
for sslmode=verify-full unless the server certificate chains to a system root;
the cluster ConfigMap pins /certs/db-ca.crt.
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.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. redis.password_file holds the raw password and is applied to the
connection directly, so it may contain any bytes (no URL encoding required).
| 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.
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.
| 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.
| 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 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 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.
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.Config's Debug impl redacts the database URL,
Redis URLs, and every password.sslmode=verify-full is refused.trusted_proxy is unset by default, so forwarded client-cert headers from an
untrusted hop are stripped before any handler reads them.0400 tmpfs file, never from
the process environment.D_OAUTH_CONFIG pointing at a missing file is not an error; the file is
optional.token_pepper of only whitespace is treated as absent.*_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.dcr_redirect_policy = open and mTLS is a deployment
error, not a load error: open registration lets a caller register an arbitrary
redirect URI.Validate any environment before starting the server:
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.
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.