ADR-0013 — bootstrap owns the admin second factor, on unless the environment is dev
Date: 2026-10-04 Status: accepted
Context
The platform admin's second factor reached a realm only through infra's realm import
(keycloak.admin_mfa, the flow browser-admin-second-factor). Keycloak skips an import for a
realm that already exists, so:
- turning the switch on for a realm in service changed nothing;
- the flow's condition named the realm role
admin, whichbootstrapnow deletes (ADR-0012). On a realm imported with the flow, the condition kept naming a role nobody holds, and platform admins signed in with a password alone, without any error.
The requester asked on 2026-10-04 that staging, and every other environment that is not dev,
have admin MFA, set up by bootstrap.
Decision
bootstrapis the one writer of the realm's browser flow (the platform level, ADR-0008). It convergesbrowser-admin-second-factorto the shape measured on Keycloak 26.7.3, conditioned onplatform-admin, and binds it, from any starting state. A condition on a retired role anywhere else is repointed toplatform-admin.CELINE_KEYCLOAK_ADMIN_MFA_REQUIREDdecides; unset, it is on unlessENVis exactlydev, the rule brute-force protection already follows. A deployment that says nothing gets the second factor; a developer's local realm does not.- The realm import no longer declares the flow. Infra drops
keycloak.admin_mfafrom its realm template and passes the environment to the bootstrap job instead. One owner, so the import and the job can never disagree on the role or the shape. - When it is off, Keycloak's own
browserflow is bound and the custom flow is left in place, unbound. Outside dev, unbinding it is destructive and needs--allow-destructive.
Consequences
- A platform admin enrols TOTP and recovery codes at the first sign-in after the first
bootstraprun with it on. The flow runs before required actions, so an admin's first password sign-in enrols TOTP before any passkey. - A flow of another shape under the same name is deleted and rebuilt. Keycloak refuses to
delete the bound browser flow, so for the length of the rebuild (a few Admin API calls) the
realm is bound to Keycloak's own
browser, which still asks a user who has TOTP for it. - The password grant is not covered. The flow is the browser flow. A client with
directAccessGrantsEnabled(oauth2_proxyhas it) still lets a platform admin's password alone obtain a token, to whoever holds that client's secret. - Recovery is by another admin, who removes both the TOTP and the recovery codes; removing
only the TOTP leaves the codes as a factor and TOTP is not enrolled again. The master realm's
admin was the break-glass account; outside dev it now has a second factor too (ADR-0014), and
the last resort is Keycloak's own
kc.sh bootstrap-admin. - The tempting undo is to put the flow back in the realm import "for new realms". That makes two writers again, and the import's copy is exactly what went stale when the role changed.