You cannot enforce MFA on AWS by asking — you enforce it with a deny policy that blocks everything except MFA setup until a device is registered, or better, by moving humans to IAM Identity Center where MFA is a directory setting instead of a policy trick. The classic enforcement policy is a Deny with "BoolIfExists": {"aws:MultiFactorAuthPresent": "false"} plus a NotAction carve-out for the self-service MFA actions. Get the condition operator wrong and the policy either locks users out of enrolling or silently enforces nothing.
What’s happening
Every authenticated AWS request carries context keys, and aws:MultiFactorAuthPresent is the one that says whether the session proved a second factor. The subtlety that breaks most homemade policies: the key is only present on temporary credentials (console sessions, AssumeRole sessions, GetSessionToken results). Requests signed with plain long-term access keys do not carry the key at all — it is not “false,” it is absent. That is why the operator matters: "Bool": {"aws:MultiFactorAuthPresent": "false"} does not match when the key is missing, so a user with access keys sails past the policy untouched. BoolIfExists treats “missing” as a match, which is what you want — CLI calls with bare keys get denied along with non-MFA console sessions. That behaviour is intentional but it means the policy also blocks service scripts running under IAM users, which is one more reason those should be roles; roles assumed by AWS services are unaffected because the policy is attached to human users, not to workload roles.
Enforcement therefore has three layers. IAM users: the deny-unless-MFA policy on a group containing every human. The root user: no policy can compel it — root MFA is a manual, non-negotiable setup step, and AWS has been progressively requiring MFA for root users of management and standalone accounts since 2024. Identity Center users: IAM Identity Center > Settings > Authentication, set MFA prompt to “Every time they sign in (always-on)” or context-aware, and registration to required at sign-in — no JSON involved. Details on the context key live in the IAM documentation.
What to check first
- Who still authenticates as an IAM user. The policy only matters for that population; Identity Center users are governed by directory settings.
- The credential report’s
mfa_activecolumn — your current enforcement gap, user by user. - Root MFA status on every account, including the forgotten sandbox accounts, visible per-account under IAM > Dashboard > Security recommendations.
- Whether any automation runs under IAM users in the human group — it will break the day the deny lands; move it to roles first.
How to verify
# Which users have no MFA device?
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 -d \
| awk -F, '$8=="false" {print $1}' # column 8 = mfa_active
# List MFA devices for one user
aws iam list-mfa-devices --user-name jsmith
# Account-wide summary including root MFA
aws iam get-account-summary --query 'SummaryMap.AccountMFAEnabled'
To test enforcement after deploying the policy: sign in as a test user without MFA and confirm the console shows Access Denied everywhere except the IAM > Users > (self) > Security credentials path; then register a device, sign out and back in, and confirm normal access returns.
The runbook
- Deploy the deny-unless-MFA policy to a group holding all human IAM users. The shape that works — abbreviated to its two decisive statements:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowOwnMfaSetup",
"Effect": "Allow",
"Action": ["iam:CreateVirtualMFADevice", "iam:EnableMFADevice",
"iam:ResyncMFADevice", "iam:ListMFADevices",
"iam:GetUser", "iam:ChangePassword"],
"Resource": ["arn:aws:iam::*:mfa/*", "arn:aws:iam::*:user/${aws:username}"]
},
{
"Sid": "DenyAllExceptSetupWithoutMFA",
"Effect": "Deny",
"NotAction": ["iam:CreateVirtualMFADevice", "iam:EnableMFADevice",
"iam:ResyncMFADevice", "iam:ListMFADevices",
"iam:GetUser", "iam:ChangePassword", "sts:GetSessionToken"],
"Resource": "*",
"Condition": {"BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}}
}
]
}
- Announce a deadline before attaching it. Users mid-task lose everything except the enrollment path the moment the policy lands; a one-week notice converts a support-ticket storm into a non-event.
- Set root MFA on every account by hand. Hardware or FIDO2 keys for the management account; store recovery information under real access control. Multiple MFA devices per root user are supported — register two.
- Flip Identity Center to always-on MFA if humans use SSO: Settings > Authentication > MFA, prompt every sign-in, require registration at first sign-in.
- Backstop with detection. A CloudTrail-based alarm on console sign-ins where
additionalEventData.MFAUsedis “No” catches anything the policy layer misses — wiring is in our CloudTrail and GuardDuty baseline.
When this is the wrong move
Perfecting IAM-user MFA is the wrong investment if the real fix — moving humans to Identity Center — is available; the policy above is scaffolding for the legacy period, not the destination (our Identity Center guide covers the move). Also skip attaching the deny to workload roles: aws:MultiFactorAuthPresent never exists for service-assumed roles, so BoolIfExists would deny your own applications.
Common mistakes
- Using
Boolinstead ofBoolIfExists, which exempts exactly the access-key users you most need to catch. - Forgetting the self-service carve-out, locking un-enrolled users out of enrolling — the policy deadlocks its own remedy.
- Enforcing MFA for the console while leaving old access keys as a policy-free side door.
- Treating root MFA on the main account as done while sandbox and legacy accounts sit unprotected.
Prevention
- Alert weekly on any
mfa_active=falserow in the credential report; the list should be empty and stay empty. - Bake MFA policy attachment into user-creation automation so no human user ever exists outside the enforced group.
- Prefer FIDO2 security keys or passkeys over SMS-free virtual TOTP where the workforce can support them; both beat no second factor.
- Re-verify root MFA after every account acquisition or org restructure — inherited accounts are where the gaps live.
MFA enforcement is one policy, one settings page, and one manual root ritual — the hard part is only deciding that “recommended” means “required.” If nobody on your team truly owns the AWS environment end to end, that is the root of most of these problems — Stack Harbor runs AWS accounts daily under AWS Operations, and you can book an AWS review.