Skip to content

Before you expose it

Work through this before anyone outside your own machine can reach the deployment.

Must be true

  • [ ] TLS everywhere, and IDEN_ENV=prod so the session cookie is Secure.
  • [ ] IDEN_ISSUER is the public HTTPS URL. It goes into every token and clients validate against it.
  • [ ] Signing keys are not in the image, are mounted read-only, and are backed up separately.
  • [ ] keys/totp.key exists and is backed up with them. It encrypts TOTP secrets at rest, and it is generated by the same scripts.gen_keys — but a deployment that copied only the .pem files finds out at the first enrolment. Lose it later and everyone with an authenticator is locked out, because they owe a factor nobody can verify.
  • [ ] The bootstrap administrator's password has been changed from the one the seed printed.
  • [ ] IDEN_FORWARDED_ALLOW_IPS names the proxy's network, and is not *. It decides whose claim about the caller's address is believed. Empty behind a proxy makes every per-address rate limit a deployment-wide one and writes the proxy into every audit row; * makes both forgeable by anyone. Verify it by signing in and reading the ip column of the audit log — it must be your own address.
  • [ ] A reverse proxy is rate-limiting by volume, and its limiter keys on the corrected address too. An uncorrected proxy limits itself as a single client, which is the whole internet in one bucket.
  • [ ] IDEN_ALLOWED_ADMIN_ORIGINS lists only origins you control. It permits credentialed cross-origin requests. On a single-origin deployment it should be empty — there is no cross-origin request to allow.
  • [ ] /docs, /redoc and /openapi.json do not answer. IDEN_ENV=prod withholds them; check rather than assume, because they are the complete shape of the admin API.
  • [ ] No service is published on a public interface. Bind to loopback or to nothing. Docker publishes ports through its own iptables chain, which a host firewall such as ufw does not cover — so a ports: entry can be reachable from the internet on a host you believe is closed. It also leaves a path to the provider that bypasses the proxy, and anything reaching it that way can forge X-Forwarded-For.
  • [ ] Database and Redis are not reachable from outside the deployment network.
  • [ ] The stores' default credentials have been replaced. IDEN_POSTGRES_PASSWORD, IDEN_REDIS_PASSWORD and IDEN_S3_SECRET_KEY each default to a value published in deploy/docker-compose.yml. Redis matters as much as PostgreSQL here and is easy to overlook: a session id is the cookie, so read access to Redis is read access to every signed-in account. Set them before the first up — PostgreSQL and the blob store keep whatever credential they were initialised with, so changing the value later does not re-key an existing volume.
  • [ ] The proxy does not strip or rewrite IDEN's response headers. The application sets Content-Security-Policy, X-Content-Type-Options, Referrer-Policy, and — when IDEN_ENV=prod — Strict-Transport-Security. A proxy that adds its own copy of any of these leaves two policies to disagree with each other.
  • [ ] Backups exist and have been restored at least once. An untested backup is a hope — see Backup and restore, which restores into a throwaway Compose project so the test cannot touch what you are running.

Should be true

  • [ ] Administrators hold admin:grants:write only if they need to assign permissions. It is the one scope that can raise somebody's authority, and it is separate from admin:users:write for that reason. An account that manages people — creating, renaming, deactivating, resetting passwords, clearing a lost authenticator — does not need it. Neither rule can be worked around by acting on somebody who already holds more: that is refused too, for people and for applications. See Scopes.
  • [ ] skipConsent is set only on applications your organization owns.
  • [ ] Every client's redirectUris are exact — no unused entries left from testing.
  • [ ] Machine clients hold the narrowest permissions that let them work.
  • [ ] backchannelLogoutUri is registered for every application with its own session, so signing out means something.
  • [ ] Someone reads GET /admin/audit on a schedule. A log nobody reads is a log nobody reads.
  • [ ] scripts/cleanup.py is scheduled, so expired codes and tokens do not accumulate forever.
  • [ ] The dashboard client's redirectUris name this deployment's origin and nothing else. The seed registers <issuer>/console/callback, and adds the two localhost development callbacks only when IDEN_ENV=dev. It is a first-party client that skips consent and may request every admin scope, so anything left over from testing is worth removing.

Known gaps

Honest rather than reassuring. Each is tracked on the roadmap.

Gap Consequence
An audit row is written just after the change If the database becomes unreachable in between, the change stands and the record is lost. Logged loudly, but lost.
Biometrics are unbuilt The permissions exist; the module does not. IDEN_BIOMETRIC_ENABLED should stay false.
No transport for outbound mail core/notifier.py logs instead of sending, and outside development it withholds the message body rather than writing a reset link to the log. Password recovery therefore does not work until a real transport replaces that function; the endpoint still answers 202, because saying otherwise would tell a caller which addresses exist.
RP-initiated logout accepts GET So a third-party page can end someone's IDEN session by loading a URL. The specification requires the GET form, and the cost is a sign-out rather than anything an attacker gains; POST is what the frontends use.

Not gaps, though they look like it

Access tokens survive revocation for up to ten minutes. They are self-contained by design — that is what lets your APIs validate them without calling IDEN. The short lifetime is the trade. For immediate revocation, use introspection on the endpoints where it earns the round trip.

Signing out on a laptop leaves a phone signed in. Revocation is per session, deliberately. To end everything, an administrator can revoke all sessions for a user; changing a password does it automatically.

You cannot remove the last administrator. Any change that would leave no active user holding admin:grants:write is refused with 409 — deactivating them, deleting them, stripping their roles, emptying the role or group that granted it. It is the only permission whose last holder is protected, because it is the only one that can restore all the others; without it the way back in is a hand-edited database.

A refresh token can be presented twice within 30 seconds. That is the grace window that stops honest double-refreshes signing people out. Detection is delayed by one rotation, never removed. Set IDEN_REFRESH_GRACE_PERIOD=0 to opt out.