Skip to content

Configuration

Every setting is read from the environment or a .env file, prefixed IDEN_. Defaults are development defaults — the ones that matter in production are called out below the table.

Where settings live

There are several .env files in the repository, which looks like more configuration than it is. Each belongs to one process, and only one of them is ever in play at a time:

File Read by In play when
deploy/.env Docker Compose, substituting ${VAR} into docker-compose.yml any docker compose run
provider/.env the provider process itself uv run provider or uv run pytest on the host
web/auth-ui/.env, web/dashboard/.env Vite pnpm dev

They hold overlapping keys but cannot hold the same values: provider/.env points the database at localhost:5432, and inside a container that address is the container itself. Each has an .env.example beside it to copy.

Every setting in the table below can be set from deploy/.env, whether or not it appears in deploy/.env.exampledocker-compose.yml names them all with their defaults. The one exception is IDEN_BOOTSTRAP_ADMIN_PASSWORD, deliberately: a value there would sit in the container's environment for as long as the container lives, where docker inspect reads it.

environment: in docker-compose.yml is not another place to configure things

That block holds two kinds of value, and only one is yours:

  • ${VAR:-default} — a deployment setting. It comes from deploy/.env, and the default is a laptop. This is the group you edit, and deploy/.env is where.
  • A plain value — wiring. postgres:5432, /keys, seaweedfs:8333 are the service names and mount points defined by that same file. There is nothing to configure; change one and it stops resolving.

Editing docker-compose.yml to set a hostname works, but it is a tracked file, so every upgrade then conflicts with your deployment. That is what deploy/.env exists to avoid.

The provider

Variable Default Notes
IDEN_ENV dev prod makes the session cookie Secure, sends HSTS, and withholds /docs, /redoc and /openapi.json.
IDEN_LOG_LEVEL info
IDEN_API_PREFIX empty Mount the whole app under a sub-path. Must stay empty otherwise — OIDC requires /.well-known/* at the host root.
IDEN_ALLOWED_ADMIN_ORIGINS [] Browser origins allowed to send credentialed requests, beyond the Auth UI. Empty is correct when everything is on one origin: same-origin calls are not cross-origin and there is nothing to permit.
IDEN_FORWARDED_ALLOW_IPS empty Addresses whose X-Forwarded-For is believed, comma-separated; CIDRs accepted. Empty trusts nobody, which is right with nothing in front. Behind a proxy, name the proxy's network — see below. Never *.
IDEN_ISSUER http://localhost:8000 The identity of this deployment. It appears in every token, and clients validate against it. Changing it invalidates everything already issued.
IDEN_AUTH_UI_BASE_URL http://localhost:4000 The origin the Auth UI is served from; /auth/login, /auth/consent and /auth/reset are appended to it. On a single-origin deployment this is the same value as IDEN_ISSUER.
IDEN_ORG_NAME empty Whose deployment this is. The same variable the two frontends read — set it on all three. Here it names the TOTP credential an authenticator app files, so it is what someone sees beside their code. Empty means IDEN.
IDEN_DATABASE_URL postgresql+asyncpg://iden:iden@localhost:5432/iden
IDEN_REDIS_URL redis://localhost:6379/0 Sessions, pending sign-ins, the denylist, and rate-limit counters.
IDEN_S3_ENDPOINT_URL empty Blob storage, over the S3 API. Empty means none is attached and profile photos are unavailable; nothing else changes.
IDEN_S3_ACCESS_KEY empty
IDEN_S3_SECRET_KEY empty
IDEN_S3_BUCKET iden Created at startup when it does not exist.
IDEN_S3_REGION us-east-1 Required by the S3 signature even where the store ignores it.
IDEN_AVATAR_MAX_BYTES 5242880 (5 MiB) Refused before the upload is decoded.
IDEN_SIGNING_KEY_DIR keys One PEM per key. Filenames sort, and the last one signs — a date-stamped name makes the newest key active.
IDEN_SIGNING_ALGORITHM RS256
IDEN_TOTP_KEY_FILE <IDEN_SIGNING_KEY_DIR>/totp.key The key that encrypts TOTP secrets at rest. Generated by scripts.gen_keys alongside the signing key. Losing it locks every enrolled account out of its second factor — back it up with the signing keys.
IDEN_ACCESS_TOKEN_TTL 600 (10 min) Short on purpose: permission changes take effect at the next issuance, and a revoked token cannot be recalled before it expires.
IDEN_ID_TOKEN_TTL 600 (10 min)
IDEN_REFRESH_TOKEN_TTL 2592000 (30 days) Sliding; rotated on every use.
IDEN_AUTH_CODE_TTL 60 (1 min) Single use.
IDEN_SESSION_TTL 86400 (24 hours) Sliding browser session.
IDEN_CHALLENGE_TTL 600 (10 min) How long a pending sign-in or consent page stays valid.
IDEN_REFRESH_GRACE_PERIOD 30 (seconds) How long a spent refresh token keeps returning what it was exchanged for. 0 restores strict single use, at the price of signing people out over a double-click.
IDEN_RATE_LIMIT_ENABLED true Off only for a load test against a deployment you own.
IDEN_DEVELOPER_MAX_CLIENTS 5 How many applications one account may register through /developer/clients. A cap on the table, not a policy — an administrator can register more on someone's behalf.
IDEN_BOOTSTRAP_ADMIN_EMAIL admin@localhost The first administrator's address. Read only when the seed creates the account; changing it later seeds a second administrator rather than renaming the first.
IDEN_BOOTSTRAP_ADMIN_PASSWORD empty Left empty, the seed generates one and prints it once. deploy/docker-compose.yml deliberately does not carry it — a password there would outlive its one use, in an environment docker inspect reads. Pass it to the seed command itself if you want to choose it. Ignored once the account exists.
IDEN_BIOMETRIC_ENABLED false Mounts /biometric/* and seeds its permissions. The module is not built yet.
IDEN_ENGINE_BASE_URL http://engine:8000

The two frontends

auth-ui and dashboard are static builds, so their configuration is read at container start rather than compiled in — one image serves any deployment. The entrypoint writes these into /config.js.

Variable Default Notes
IDEN_ISSUER http://localhost:8000 The provider's origin, which is what these apps call. If it differs from the origin the app itself is served from, that serving origin is the one that has to appear in IDEN_ALLOWED_ADMIN_ORIGINS — CORS permits the caller, not the callee.
IDEN_ORG_NAME empty Whose sign-in page this is. Takes the larger type wherever both appear, with IDEN as a caption beneath. Empty and IDEN stands alone. The provider reads the same variable — give all three services the same value.
IDEN_ORG_LOGO_URL empty Any URL the browser can reach. Sits beside the name.

Under pnpm dev there is no container, so the same three are read from Vite environment variables with a VITE_ prefix — VITE_IDEN_ISSUER, VITE_IDEN_ORG_NAME, VITE_IDEN_ORG_LOGO. Each app has an .env.example to copy.

The four that matter in production

IDEN_ISSUER is the identity of the deployment. It goes into every token and every client validates against it. Set it to the public HTTPS URL, and treat changing it as invalidating every token in circulation.

IDEN_SIGNING_KEY_DIR holds the private keys that sign every token. Anyone who reads them can mint a token for anyone. Mount it read-only, keep it off the image, and back it up somewhere you would be comfortable keeping a password.

IDEN_TOTP_KEY_FILE encrypts the one credential IDEN cannot hash. Verifying a time-based code means recomputing it, so the secret has to be readable — and without this a database dump would be a working second factor for every enrolled account, while the passwords beside it are argon2. The key lives in the signing key directory by default, which is already mounted read-only, kept out of the image and backed up. Replace it and every enrolled authenticator stops verifying, and those people cannot sign in until an administrator clears their enrolment.

IDEN_ENV=prod makes the session cookie Secure, so it is never sent over plain HTTP. It also withholds the interactive docs, which are the complete shape of the admin API.

IDEN_FORWARDED_ALLOW_IPS decides who the caller is. IDEN reads the address from the socket unless the peer is named here, in which case it takes the address that peer states in X-Forwarded-For. Both the per-address rate limits and the ip column of the audit log rest on the answer.

Empty behind a proxy makes every per-address limit a deployment-wide one — TOKEN_PER_IP stops being 120 requests a minute per caller and becomes 120 a minute in total — and writes the proxy's address into every audit row. * accepts the header from anyone, which makes both forgeable. Name the proxy's network, and no more of it than necessary.

Rotating a signing key

Keys sort by filename and the last one signs, so a date-stamped name makes the newest key active:

keys/
  iden-20260101.pem     ← was signing
  iden-20260801.pem     ← now signing

Keep the old key in place until every token signed with it has expired — it is still published in JWKS, so tokens already issued keep validating. Then delete it.