Fixing IAM User Console Login 'Access Denied' After Password Policy Change in AWS
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke: An IAM account-level password policy was tightened (stricter length, complexity, or
HardExpiry=true), and existing user passwords that predate the new policy are now rejected or force-expired on next login attempt — producing a silentAccess Deniedor an immediate forced-reset loop that blocks console access entirely. - How to fix it: Identify the specific policy field causing the rejection, either roll back that field temporarily or force a one-time admin password reset for affected users, then re-communicate the rotation requirement via a controlled change window.
- Fast path: Use our Client-Side Sandbox above to paste your current IAM password policy JSON and get an auto-refactored policy with the conflicting fields highlighted and corrected.
The Incident (What Does the Error Mean?)
The user hits https://<account-id>.signin.aws.amazon.com/console, enters valid credentials, and receives:
Access Denied
Your request included invalid authentication credentials for the AWS Management Console.
Please try again.
or, on some policy configurations, is immediately redirected to a forced password-change screen that rejects every new password the user submits — because the new policy's complexity rules are not clearly surfaced in the UI error message.
Immediate consequence: The IAM user is effectively locked out of the console. If HardExpiry is set to true, the account is hard-blocked — no API calls, no console, no CLI using long-term credentials — until an IAM administrator resets the password. This is not a soft warning. The account is dead until manual intervention.
Raw policy state that triggers this (example):
{
"MinimumPasswordLength": 14,
"RequireSymbols": true,
"RequireNumbers": true,
"RequireUppercaseCharacters": true,
"RequireLowercaseCharacters": true,
"MaxPasswordAge": 1,
"PasswordReusePrevention": 24,
"HardExpiry": true
}
MaxPasswordAge: 1 combined with HardExpiry: true means any password older than 24 hours hard-locks the account. If this policy was pushed on a Friday afternoon, every user who didn't log in over the weekend is locked out Monday morning.
The Attack Vector / Blast Radius
This is a self-inflicted availability attack with a secondary security risk surface:
1. Mass lockout blast radius:
A single update-account-password-policy call applies to every IAM user in the account simultaneously. There is no staged rollout. If your account has 200 IAM users and HardExpiry=true with MaxPasswordAge=1, you've just scheduled a mass lockout event. In organizations without SSO/IdP federation, this means 200 support tickets at 9 AM.
2. Privilege escalation window:
When administrators scramble to fix lockouts, they frequently grant temporary AdministratorAccess or reset passwords without MFA verification — creating a social engineering opportunity. An attacker who knows a policy change is in flight can call the help desk impersonating a locked-out executive.
3. Break-glass account exposure: Teams often respond to mass lockouts by using a shared break-glass root or admin account. If that account's credentials are not rotated and properly vaulted, the incident itself becomes the attack vector.
4. HardExpiry is irreversible from the user side:
Unlike a soft expiry (which prompts a password change on login), HardExpiry: true returns Access Denied with no recovery path for the user. Only an IAM principal with iam:UpdateLoginProfile can unlock them. If your only admin is also locked out, you are escalating to root account recovery.
How to Fix It (The Solution)
Basic Fix — Emergency Unlock a Specific User
If a single user is locked out, reset their password immediately via CLI:
aws iam update-login-profile \
--user-name <locked-username> \
--password "TempP@ssw0rd!2024" \
--password-reset-required
This forces a password change on next login and bypasses the hard expiry block. Requires iam:UpdateLoginProfile permission on the calling principal.
Enterprise Best Practice — Fix the Root Policy
The actual fix is correcting the policy that caused the lockout. The two most dangerous fields are HardExpiry and an unrealistically short MaxPasswordAge.
# aws iam update-account-password-policy (effective policy diff)
{
"MinimumPasswordLength": 14,
"RequireSymbols": true,
"RequireNumbers": true,
"RequireUppercaseCharacters": true,
"RequireLowercaseCharacters": true,
- "MaxPasswordAge": 1,
+ "MaxPasswordAge": 90,
- "PasswordReusePrevention": 24,
+ "PasswordReusePrevention": 12,
- "HardExpiry": true
+ "HardExpiry": false
}
Why these specific changes:
MaxPasswordAge: 90— aligns with NIST SP 800-63B guidance (periodic rotation is less important than breach detection; 90 days is the enterprise compliance floor for most frameworks including SOC 2 and ISO 27001).PasswordReusePrevention: 12— 24 previous passwords is excessive and causes users to cycle through trivial variations; 12 is the PCI-DSS requirement and a defensible audit position.HardExpiry: false— never set this to true in production without a tested, automated unlock workflow in place. Soft expiry prompts a change on login; hard expiry kills the account silently.
Apply via CLI:
aws iam update-account-password-policy \
--minimum-password-length 14 \
--require-symbols \
--require-numbers \
--require-uppercase-characters \
--require-lowercase-characters \
--max-password-age 90 \
--password-reuse-prevention 12 \
--no-hard-expiry
Verify the applied policy:
aws iam get-account-password-policy
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
Password policy changes should never be a manual aws iam update-account-password-policy call in production. Treat this as infrastructure.
1. Enforce via Terraform with guardrails
# terraform/iam_password_policy.tf
resource "aws_iam_account_password_policy" "strict" {
minimum_password_length = 14
require_symbols = true
require_numbers = true
require_uppercase_characters = true
require_lowercase_characters = true
max_password_age = 90
password_reuse_prevention = 12
- hard_expiry = true
+ hard_expiry = false
}
2. Block dangerous policy values with Checkov
Add a custom Checkov check or use the built-in CKV_AWS_9 (password policy expiry) and CKV_AWS_10 (minimum length). Add a custom policy for hard_expiry:
# .checkov/custom_checks/no_hard_expiry.yaml
metadata:
name: "IAM password policy must not set hard_expiry to true"
id: "CKV2_AWS_CUSTOM_001"
severity: HIGH
scope:
provider: aws
definition:
and:
- cond_type: attribute
resource_types: ["aws_iam_account_password_policy"]
attribute: hard_expiry
operator: equals
value: false
3. Gate policy changes with AWS Config Rule
Enable the managed Config rule iam-password-policy and configure it with your required parameter values. Any drift from the approved policy triggers a finding in Security Hub within minutes — before the lockout wave hits.
aws configservice put-config-rule --config-rule '{
"ConfigRuleName": "iam-password-policy",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "IAM_PASSWORD_POLICY"
},
"InputParameters": "{\"MaxPasswordAge\":\"90\",\"MinimumPasswordLength\":\"14\",\"RequireSymbols\":\"true\",\"HardExpiry\":\"false\"}"
}'
4. Change management rule (non-negotiable)
Before any update-account-password-policy call in production:
- Run
aws iam generate-credential-reportand identify all users with passwords older than the newMaxPasswordAge. - Send forced-reset notifications to those users before applying the policy.
- Apply the policy during a maintenance window, not ad hoc.
- Have a runbook pre-written for
iam:UpdateLoginProfilebulk resets if the rollout fails.