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=prodso the session cookie isSecure. - [ ]
IDEN_ISSUERis 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.keyexists and is backed up with them. It encrypts TOTP secrets at rest, and it is generated by the samescripts.gen_keys— but a deployment that copied only the.pemfiles 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_IPSnames 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 theipcolumn 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_ORIGINSlists 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,/redocand/openapi.jsondo not answer.IDEN_ENV=prodwithholds 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 forgeX-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_PASSWORDandIDEN_S3_SECRET_KEYeach default to a value published indeploy/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 firstup— 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 — whenIDEN_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:writeonly if they need to assign permissions. It is the one scope that can raise somebody's authority, and it is separate fromadmin:users:writefor 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. - [ ]
skipConsentis set only on applications your organization owns. - [ ] Every client's
redirectUrisare exact — no unused entries left from testing. - [ ] Machine clients hold the narrowest permissions that let them work.
- [ ]
backchannelLogoutUriis registered for every application with its own session, so signing out means something. - [ ] Someone reads
GET /admin/auditon a schedule. A log nobody reads is a log nobody reads. - [ ]
scripts/cleanup.pyis scheduled, so expired codes and tokens do not accumulate forever. - [ ] The
dashboardclient'sredirectUrisname this deployment's origin and nothing else. The seed registers<issuer>/console/callback, and adds the twolocalhostdevelopment callbacks only whenIDEN_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.