Require MFA for admin accounts
Different policies for admins vs users
A common compliance ask: "All admin accounts must have MFA." Implementation in Olympus.
Policy in kratos.yml
Kratos doesn't natively support "MFA required when role = admin", but we can configure it via hooks and session AAL requirements.
# kratos.yml
session:
whoami:
required_aal: highest_available # session uses highest method enrolledhighest_available means: if user has MFA enrolled, login must reach AAL2. Without MFA enrolled, AAL1 is fine.
For admins, we want stricter: must reach AAL2 always, must have MFA enrolled.
Pre-login hook
selfservice:
flows:
login:
before:
hooks:
- hook: web_hook
config:
url: http://your-backend/check-admin-mfa
response:
ignore: false
parse: trueBackend:
export async function POST(req: Request) {
const { identity } = await req.json();
if (identity.traits.role !== "admin") {
return Response.json({ ok: true });
}
// Admin must have MFA enrolled
const credentials = await kratos.adminListCredentials(identity.id);
const hasMfa = credentials.some(c =>
c.type === "totp" || c.type === "webauthn" || c.type === "lookup_secret"
);
if (!hasMfa) {
return Response.json({
reject: true,
redirect_to: "/settings?required=mfa",
error: "admin_must_enroll_mfa",
});
}
return Response.json({ ok: true });
}Admin without MFA → can't complete login. Redirected to settings to enroll.
Post-login: enforce AAL2
session:
required_aal: aal2 # for admin pathsBut this applies globally. Better: per-route enforcement in Athena's middleware.
// athena/middleware.ts
const session = await kratos.toSession({ cookie });
if (session.identity.traits.role === "admin" && session.authenticator_assurance_level !== "aal2") {
return NextResponse.redirect("/self-service/login?aal=aal2");
}Admin path requires AAL2. If user logged in with password only, they're redirected to step-up MFA.
Enforce in your app too
If your app has admin features, gate them similarly:
function requireAdmin(req) {
if (req.user?.role !== "admin") throw new Forbidden();
if (req.user?.aal !== "aal2") throw new StepUpRequired();
}
app.get("/admin/*", requireAdmin, ...);Send aal claim in session/JWT so apps can check.
Grandfathering existing admins
If you're rolling out MFA-required for existing admins:
- Today: send notice to all admins. "MFA required by [date]."
- Day -7: warning banner in app.
- Day 0: hook enforces. Admins without MFA can't log in.
- They go through enrollment.
See Feature flag MFA rollout for more.
Allowed MFA methods for admins
Restrict to phishing-resistant:
const acceptable = ["webauthn", "passkey"]; // not TOTP, not SMS
const ok = credentials.some(c => acceptable.includes(c.type));Admins are high-value targets. TOTP can be phished (the user types it into a phishing page). WebAuthn (origin-bound) cannot.
For maximum security: require hardware key (YubiKey). For most: WebAuthn (passkey).
Audit
Log every MFA enrollment and removal:
INSERT INTO security_audit (event, actor_id, target_id, metadata)
VALUES ('mfa_enrolled', $identity_id, $identity_id, '{"method": "webauthn"}');If an admin disables their MFA, alarm bells:
await alert("Admin disabled MFA: {{user}}");Investigate. Phish? Compromise? Stupid?
Block self-disable
Admins should not be able to disable their own MFA without a second admin's approval (4-eyes principle).
Implementation:
- Settings flow hook on MFA-disable for admin role: reject.
- "To disable MFA, contact another admin."
// pre-settings hook
if (operation === "mfa_disable" && identity.traits.role === "admin") {
return Response.json({ reject: true, error: "two_admin_required" });
}Admin requests disable → another admin approves via Athena → 2-of-N approval recorded → then disabled.
For small teams, this is overkill. For regulated industries: required.
"Cannot enroll MFA" edge cases
- Admin lost MFA device, can't sign in.
- Backup codes were never saved.
Recovery: another admin (or break-glass account) resets. See Locked account unlock.
Communicating to users
Subject: 2-factor authentication required for admin accounts
As of [date], all admin accounts require 2-factor authentication.
Enroll now:
1. Sign in at https://...
2. Settings → Security → Set up 2FA.
3. Save your backup codes.
If you don't enroll by [date], you won't be able to sign in until you do.
Why we're doing this: admin accounts are high-value targets. 2FA blocks
attacks that compromise just your password.