To check the IAM password policy, call GetAccountPasswordPolicy. A NoSuchEntity error means no custom policy exists and the AWS default applies. Compare MinimumPasswordLength and PasswordReusePrevention with the CIS baseline of 14 characters and 24 remembered passwords, then fix gaps with UpdateAccountPasswordPolicy, sending every setting, because omitted ones reset to defaults.
The account password policy is one of the first items in the CIS AWS Foundations Benchmark and in Security Hub’s IAM controls, so it shows up in almost every audit. It’s also easy to break: the update call has no partial update, and a script that sends only the minimum length quietly turns off password reuse prevention.
This example shows how to check IAM password policy settings from code, how many IAM users the policy actually affects, and how to update it without side effects. It fits with the scripts to find IAM users without MFA and to check the AWS root user for MFA and access keys, which cover the rest of the sign-in picture.
What does the IAM password policy cover?
Less than people expect. It applies only to console passwords of IAM users in this account:
- Not the root user. The policy doesn’t apply to the AWS account root user password.
- Not access keys. A user whose password expired can still call the API with their access keys.
- Not IAM Identity Center. Users in the Identity Center directory follow fixed rules: 8 to 64 characters, all four character types, and the last three passwords can’t be reused. Federated users follow your identity provider. Amazon Cognito user pools have their own password and MFA settings too, which the script to find Cognito user pools where MFA is not enabled checks.
- No lockout. You can’t configure an account lockout after failed sign-ins. MFA is the compensating control.
Timing matters too. Length and character-type rules apply the next time each user changes their password; existing passwords aren’t checked. An expiration period applies immediately, so setting 90 days forces every user with an older password to change it at next sign-in.
Without a custom policy, IAM uses its default: 8 to 128 characters, at least three of uppercase, lowercase, numbers and symbols, not the same as the account name or email, and no expiry.
What should the policy say? CIS and NIST disagree on some points
| Setting | CIS AWS Foundations Benchmark | NIST SP 800-63B | Script default |
|---|---|---|---|
| Minimum length | 14 or more | 15 for password-only sign-in, 8 when used with MFA | 14 (--min-length) |
| Password reuse | Prevent reuse (Security Hub IAM.16 checks for 24) | Not specified; blocklist of compromised passwords instead | 24 (--reuse) |
| Character types | Dropped after v1.2.0 | Must not be required | Reported only (--require-classes to enforce) |
| Expiry | Dropped after v1.2.0 | Must not force periodic changes, only on compromise | Reported only |
The CIS Amazon Web Services Foundations Benchmark is free to download for non-commercial use; in v5.0.0 the two password recommendations are 1.7 and 1.8. NIST SP 800-63B (August 2025) says verifiers shall not impose composition rules or periodic changes. Both agree that long passwords plus MFA beat complexity rules, which is why the script enforces length and reuse and only reports the rest. If a compliance framework you follow still requires character classes or 90-day expiry, pass --require-classes or set the expiry by hand.
What does the script do?
- Reads the policy
GetAccountPasswordPolicy, treatingNoSuchEntityExceptionas “IAM default in force”. - Compares itMinimum length, reuse prevention and whether users may change their own password are pass or fail. Expiry, hard expiry and character classes are shown for information.
- Counts affected users
paginateListUsersplusGetLoginProfileper user shows how many IAM users have a console password at all. - Prints the fixWithout
--applyit prints the full request it would send. With--applyit callsUpdateAccountPasswordPolicywith every field, built from the current policy.
Prerequisites
- Node.js 18 or later, npm,
tsxand@aws-sdk/client-iam. - An AWS profile per account you check; the guide to AWS SDK v3 credential providers such as fromSSO and assume role shows how to loop over accounts.
- At least two people who can reset passwords, if you ever turn on
HardExpiry.
Which IAM permissions does it need?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadPasswordPolicy",
"Effect": "Allow",
"Action": [
"iam:GetAccountPasswordPolicy",
"iam:ListUsers"
],
"Resource": "*"
},
{
"Sid": "ReadLoginProfiles",
"Effect": "Allow",
"Action": "iam:GetLoginProfile",
"Resource": "arn:aws:iam::123456789012:user/*"
},
{
"Sid": "UpdatePolicyWithApply",
"Effect": "Allow",
"Action": "iam:UpdateAccountPasswordPolicy",
"Resource": "*"
}
]
}
The password policy actions don’t take a resource ARN, so they use "*". Remove the last statement for an audit role. The IAM policy generator for TypeScript code can confirm the list from the script itself.
The script to check IAM password policy settings
// check-iam-password-policy.ts
// Reads the account password policy for IAM users, compares it with a baseline (CIS: length 14 or more,
// 24 remembered passwords) and counts the IAM users who actually have a console password.
// Report only unless you pass --apply, which writes a complete policy that meets the baseline.
// Usage:
// npx tsx check-iam-password-policy.ts [--min-length 14] [--reuse 24] [--require-classes] [--apply]
import {
GetAccountPasswordPolicyCommand,
GetLoginProfileCommand,
IAMClient,
NoSuchEntityException,
UpdateAccountPasswordPolicyCommand,
paginateListUsers,
type PasswordPolicy,
type UpdateAccountPasswordPolicyCommandInput,
} from "@aws-sdk/client-iam";
const args = process.argv.slice(2);
const flag = (name: string): string | undefined => {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
};
const minLength = Number(flag("--min-length") ?? "14");
const reuse = Number(flag("--reuse") ?? "24");
const requireClasses = args.includes("--require-classes"); // also require upper, lower, number and symbol
const apply = args.includes("--apply");
const iam = new IAMClient({ region: process.env.AWS_REGION ?? "us-east-1" }); // IAM is global
interface Check {
Setting: string;
Current: string;
Baseline: string;
Result: string;
}
async function currentPolicy(): Promise<PasswordPolicy | undefined> {
try {
const { PasswordPolicy } = await iam.send(new GetAccountPasswordPolicyCommand({}));
return PasswordPolicy;
} catch (err) {
if (err instanceof NoSuchEntityException) return undefined; // no custom policy: the IAM default applies
throw err;
}
}
async function consoleUsers(): Promise<{ total: number; withPassword: number }> {
let total = 0;
let withPassword = 0;
for await (const page of paginateListUsers({ client: iam }, {})) {
for (const u of page.Users ?? []) {
total++;
try {
await iam.send(new GetLoginProfileCommand({ UserName: u.UserName }));
withPassword++;
} catch (err) {
if (!(err instanceof NoSuchEntityException)) throw err;
}
}
}
return { total, withPassword };
}
function evaluate(p: PasswordPolicy | undefined): Check[] {
const show = (v: number | boolean | undefined) => (v === undefined ? "not set" : String(v));
const checks: Check[] = [
{ Setting: "MinimumPasswordLength", Current: show(p?.MinimumPasswordLength), Baseline: `>= ${minLength}`, Result: (p?.MinimumPasswordLength ?? 0) >= minLength ? "ok" : "FAIL" },
{ Setting: "PasswordReusePrevention", Current: show(p?.PasswordReusePrevention), Baseline: `>= ${reuse}`, Result: (p?.PasswordReusePrevention ?? 0) >= reuse ? "ok" : "FAIL" },
{ Setting: "AllowUsersToChangePassword", Current: show(p?.AllowUsersToChangePassword), Baseline: "true", Result: p?.AllowUsersToChangePassword ? "ok" : "FAIL" },
{ Setting: "MaxPasswordAge", Current: show(p?.MaxPasswordAge), Baseline: "info only", Result: p?.ExpirePasswords ? `passwords expire after ${p.MaxPasswordAge} days` : "no expiry" },
{ Setting: "HardExpiry", Current: show(p?.HardExpiry), Baseline: "info only", Result: p?.HardExpiry ? "admin must reset expired passwords" : "users can reset expired passwords" },
];
const classes = [p?.RequireUppercaseCharacters, p?.RequireLowercaseCharacters, p?.RequireNumbers, p?.RequireSymbols];
checks.push({
Setting: "Require upper/lower/number/symbol",
Current: classes.map((c) => (c ? "Y" : "N")).join("/"),
Baseline: requireClasses ? "Y/Y/Y/Y" : "info only",
Result: requireClasses && !classes.every(Boolean) ? "FAIL" : "ok",
});
return checks;
}
// UpdateAccountPasswordPolicy has no partial update: any parameter left out reverts to its default,
// so the new policy is built from the current one. MaxPasswordAge and PasswordReusePrevention can't be
// sent as 0; leaving them out is how you ask for "never expire" and "no reuse check".
function desiredPolicy(p: PasswordPolicy | undefined): UpdateAccountPasswordPolicyCommandInput {
const input: UpdateAccountPasswordPolicyCommandInput = {
MinimumPasswordLength: Math.max(p?.MinimumPasswordLength ?? 0, minLength),
PasswordReusePrevention: Math.max(p?.PasswordReusePrevention ?? 0, reuse),
AllowUsersToChangePassword: true,
HardExpiry: p?.HardExpiry ?? false,
RequireUppercaseCharacters: requireClasses || (p?.RequireUppercaseCharacters ?? false),
RequireLowercaseCharacters: requireClasses || (p?.RequireLowercaseCharacters ?? false),
RequireNumbers: requireClasses || (p?.RequireNumbers ?? false),
RequireSymbols: requireClasses || (p?.RequireSymbols ?? false),
};
if (p?.ExpirePasswords && p.MaxPasswordAge) input.MaxPasswordAge = p.MaxPasswordAge;
return input;
}
async function main(): Promise<void> {
const policy = await currentPolicy();
console.log(policy ? "Custom account password policy found." : "No custom password policy (NoSuchEntity): the IAM default policy applies.");
const checks = evaluate(policy);
console.table(checks);
const users = await consoleUsers();
console.log(`${users.withPassword} of ${users.total} IAM users have a console password.`);
const failed = checks.filter((c) => c.Result === "FAIL").length;
if (!failed) {
console.log("The policy meets the baseline.");
return;
}
const input = desiredPolicy(policy);
if (!apply) {
console.log(`${failed} setting(s) below baseline. --apply would send:`);
console.log(JSON.stringify(input, null, 2));
return;
}
await iam.send(new UpdateAccountPasswordPolicyCommand(input));
console.log("Password policy updated. New rules apply the next time each user sets a password.");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Why build the request from the current policy? UpdateAccountPasswordPolicy doesn’t support partial updates: a parameter you leave out reverts to its default, such as a minimum length of 6 or no reuse check. MaxPasswordAge and PasswordReusePrevention also can’t be sent as 0; omitting them is the only way to say “never” or “off”.
How do you run it?
npm install @aws-sdk/client-iam
npm install --save-dev tsx typescript @types/node
# Check against CIS defaults
AWS_PROFILE=readonly npx tsx check-iam-password-policy.ts
# Follow NIST's 15 characters instead, then apply
AWS_PROFILE=iam-admin npx tsx check-iam-password-policy.ts --min-length 15 --apply
Sample output
No custom password policy (NoSuchEntity): the IAM default policy applies.
┌─────────┬─────────────────────────────────────┬───────────┬─────────────┬─────────────────────────────────────┐
│ (index) │ Setting │ Current │ Baseline │ Result │
├─────────┼─────────────────────────────────────┼───────────┼─────────────┼─────────────────────────────────────┤
│ 0 │ 'MinimumPasswordLength' │ 'not set' │ '>= 14' │ 'FAIL' │
│ 1 │ 'PasswordReusePrevention' │ 'not set' │ '>= 24' │ 'FAIL' │
│ 2 │ 'AllowUsersToChangePassword' │ 'not set' │ 'true' │ 'FAIL' │
│ 3 │ 'MaxPasswordAge' │ 'not set' │ 'info only' │ 'no expiry' │
│ 4 │ 'HardExpiry' │ 'not set' │ 'info only' │ 'users can reset expired passwords' │
│ 5 │ 'Require upper/lower/number/symbol' │ 'N/N/N/N' │ 'info only' │ 'ok' │
└─────────┴─────────────────────────────────────┴───────────┴─────────────┴─────────────────────────────────────┘
3 of 11 IAM users have a console password.
3 setting(s) below baseline. --apply would send:
{
"MinimumPasswordLength": 14,
"PasswordReusePrevention": 24,
"AllowUsersToChangePassword": true,
"HardExpiry": false,
"RequireUppercaseCharacters": false,
"RequireLowercaseCharacters": false,
"RequireNumbers": false,
"RequireSymbols": false
}
This account has no custom policy, so the default applies. Note what --apply would change beyond length and reuse: the default’s “three of four character types” rule goes away, because a custom policy with all four Require* flags false has no composition rule. That matches NIST; add --require-classes if your auditor expects character classes. And only 3 of 11 users have a console password, so the real fix may be moving those three to IAM Identity Center.
What else should you check next to the password policy?
- Console users without MFA, with the script to find IAM users without MFA using AWS SDK v3.
- Old keys the password policy doesn’t touch: find IAM access keys older than 90 days or never used.
- Permissions attached straight to users: find IAM users with directly attached policies.
- Account-wide findings in one place: check Security Hub is enabled in every AWS Region, whose CIS standard includes the password controls.
Troubleshooting
NoSuchEntityExceptionin your own code. That’s not a failure; it means the account uses the default policy. Catch it, as the script does.- A validation error on update. A value is out of range: length must be 6 to 128, reuse 1 to 24, and maximum age 1 to 1,095 days.
- Users locked out after setting an expiry. Expiry applies at once to passwords older than the period. Users need
iam:ChangePassword(granted byAllowUsersToChangePassword), or an admin must reset them ifHardExpiryis on. AccessDenied. A service control policy may block IAM changes in member accounts; see troubleshoot AWS IAM access denied errors step by step.
Ask ChatWithCloud instead
For a one-off check, ask ChatWithCloud “What is the IAM account password policy, and which IAM users have console passwords?” It writes AWS SDK for JavaScript v2 code, runs it locally with your AWS profile and explains what it found; how ChatWithCloud runs AWS SDK code on your machine covers the details. Generated code runs without a confirmation step, so use a read-only AWS profile for ChatWithCloud and make the update with the script. The guide to analyze AWS security posture with an AI CLI has more IAM questions.
Frequently asked questions
How do I check the IAM password policy in the AWS CLI?
Run aws iam get-account-password-policy. If it returns NoSuchEntity, no custom policy is set and the IAM default applies.
What is the default IAM password policy?
At least 8 characters (up to 128), at least three of uppercase, lowercase, numbers and symbols, not equal to the account name or email, and passwords never expire.
Does the IAM password policy apply to the root user?
No. It applies only to IAM user console passwords. Protect the root user with a strong password and MFA.
Do existing passwords have to meet a new policy?
No. Length and character rules apply at the next password change. Only a new expiration period takes effect immediately.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud