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.example — docker-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 fromdeploy/.env, and the default is a laptop. This is the group you edit, anddeploy/.envis where.- A plain value — wiring.
postgres:5432,/keys,seaweedfs:8333are 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:
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.