cmc/cleberg.net
My personal web garden & blog.
clone: git clone https://gitbay.org/cmc/cleberg.net.git
main: content/blog/2026-02-21-auditing-aws-passwords.org · raw
1#+date: [2026-02-21 Sat 21:13:45]
2#+title: Auditing AWS Passwords
3#+description: How to audit password policies and usage in AWS.
4#+slug: auditing-aws-passwords
5#+filetags: :audit:
6
7One of the first controls an IT Auditor learns is how to audit passwords in any
8number of IT systems. However, things have changed with the introduction of
9cloud platforms. This post covers the process and results of auditing AWS
10passwords.
11
12The scripts are available at [[https://github.com/audit-labs/audit-tools/tree/main/applications/aws/aws_password_policy][audit-labs/audit-tools]].
13
14* Scoping
15
16First thing first: scoping. To audit AWS passwords, we need to understand that
17AWS IAM password policies only apply to users with console access. This isn't
18the same as the password policy for an application built on top of AWS. It also
19has no effect on users who authenticate through IAM Identity Center, which
20delegates authentication to an external identity provider, such as Okta or
21Active Directory.
22
23If the organization uses Identity Center exclusively and has no IAM users with
24passwords, you will need to perform a different procedure.
25
26If they do have IAM users with console access, the script applies and is worth
27testing.
28
29* What the Scripts Do
30
31The process runs in two steps:
321. ~gather_policy.sh~ calls the AWS CLI to fetch the current IAM password policy,
33 captures metadata (timestamp, AWS account, region, caller identity), and
34 writes everything to a JSON file. This file is evidence of the policy's
35 current state.
362. ~evaluate_policy.py~ reads that JSON file, prompts you for the expected value
37 of each setting, and exports a CSV report with a value of ~PASS~ or ~FAIL~ for
38 each rule.
39
40* Prerequisites
41
42To run this script, you'll need:
43
44- Access to CloudShell or the AWS CLI utility installed and configured with
45 credentials that have read access to ~iam:GetAccountPasswordPolicy~ and
46 ~sts:GetCallerIdentity~
47- ~jq~ installed (used by the Bash script to merge JSON objects)
48- Python 3 installed
49- Optional: ~uv~ installed (used to run the Python script)
50
51* Step 1: Gather the Policy
52
53Run the first script to pull the current policy from AWS:
54
55#+begin_src bash
56chmod +x gather_policy.sh
57./gather_policy.sh
58#+end_src
59
60By default, this writes the output to ~policy_report.json~ in the current
61directory. You can specify a custom path with the ~-o~ flag:
62
63#+begin_src bash
64./gather_policy.sh -o /tmp/my_report.json
65#+end_src
66
67The output is a JSON file with two top-level keys: ~metadata~ and ~PasswordPolicy~.
68
69#+begin_src json
70{
71 "metadata": {
72 "report_timestamp_utc": "2025-12-15T01:29:52Z",
73 "os_user": "cloudshell-user",
74 "hostname": "",
75 "working_directory": "/home/cloudshell-user",
76 "aws_profile": "default",
77 "aws_region": "eu-west-1",
78 "aws_caller_identity": {
79 "UserId": "214941490075",
80 "Account": "214941490075",
81 "Arn": "arn:aws:iam::214941490075:root"
82 }
83 },
84 "PasswordPolicy": {
85 "MinimumPasswordLength": 8,
86 "RequireSymbols": true,
87 "RequireNumbers": true,
88 "RequireUppercaseCharacters": true,
89 "RequireLowercaseCharacters": true,
90 "AllowUsersToChangePassword": true,
91 "ExpirePasswords": true,
92 "MaxPasswordAge": 90,
93 "PasswordReusePrevention": 4,
94 "HardExpiry": false
95 }
96}
97#+end_src
98
99The metadata block is what ties this evidence to a specific account and point in
100time. The ~aws_caller_identity~ field shows who ran the script and in which
101account.
102
103#+caption: Policy Evaluation Results
104#+attr_html: :alt Terminal output of evaluate_policy.py showing the interactive prompts and pass/fail summary for each password policy rule.
105[[https://img.cleberg.net/blog/20260221-auditing-aws-passwords/results.webp]]
106
107* Step 2: Evaluate the Policy
108
109Pass the JSON file to the evaluation script:
110
111#+begin_src bash
112uv run evaluate_policy.py policy_report.json
113#+end_src
114
115The script will prompt you for each of the ten settings. Press ~Enter~ to skip any
116setting you don't need to test. For numeric settings like ~MinimumPasswordLength~
117and ~MaxPasswordAge~, the script treats your input as a minimum, so the actual
118value must be greater than or equal to your expected value to pass. Boolean
119settings require an exact match.
120
121#+begin_src text
122=== Expected / Minimum Values (press <Enter> for N/A) ===
123
124Enter expected value for 'Minimum password length' (int) or press <Enter> to skip: 8
125Enter expected value for 'Require symbols (!@#$...)' (bool) or press <Enter> to skip: true
126Enter expected value for 'Require numbers (0-9)' (bool) or press <Enter> to skip: true
127Enter expected value for 'Require uppercase letters (A-Z)' (bool) or press <Enter> to skip: true
128Enter expected value for 'Require lowercase letters (a-z)' (bool) or press <Enter> to skip: true
129Enter expected value for 'Allow users to change password' (bool) or press <Enter> to skip: true
130Enter expected value for 'Expire passwords (enable aging)' (bool) or press <Enter> to skip: true
131Enter expected value for 'Maximum password age (days)' (int) or press <Enter> to skip: 90
132Enter expected value for 'Prevent password reuse (last N)' (int) or press <Enter> to skip: 4
133Enter expected value for 'Hard expiry (no grace period)' (bool) or press <Enter> to skip: false
134
135Audit CSV written to: policy_audit_20251215T014323Z.csv
136
137Summary:
138 1. Minimum password length -> PASS
139 2. Require symbols (!@#$...) -> PASS
140 3. Require numbers (0-9) -> PASS
141 4. Require uppercase letters (A-Z) -> PASS
142 5. Require lowercase letters (a-z) -> PASS
143 6. Allow users to change password -> PASS
144 7. Expire passwords (enable aging) -> PASS
145 8. Maximum password age (days) -> PASS
146 9. Prevent password reuse (last N) -> PASS
147 10. Hard expiry (no grace period) -> PASS
148
149--- End of report ---
150#+end_src
151
152* Reading the CSV
153
154The CSV is your evidence. It includes the metadata header from the JSON file, so
155the account ID, timestamp, and caller identity are embedded directly in the
156file.
157
158#+begin_src csv
159# report_timestamp_utc: 2025-12-15T01:29:52Z
160# os_user: cloudshell-user
161# hostname:
162# working_directory: /home/cloudshell-user
163# aws_profile: default
164# aws_region: eu-west-1
165# aws_caller_identity: {'UserId': '214941490075', 'Account': '214941490075', 'Arn': 'arn:aws:iam::214941490075:root'}
166
167Rule#,Policy-Item,Expected,Actual,Result
1681,Minimum password length,8,8,PASS
1692,Require symbols (!@#$...),true,true,PASS
1703,Require numbers (0-9),true,true,PASS
1714,Require uppercase letters (A-Z),true,true,PASS
1725,Require lowercase letters (a-z),true,true,PASS
1736,Allow users to change password,true,true,PASS
1747,Expire passwords (enable aging),true,true,PASS
1758,Maximum password age (days),90,90,PASS
1769,Prevent password reuse (last N),4,4,PASS
17710,Hard expiry (no grace period),false,false,PASS
178#+end_src
179
180You may use this evidence in any form, but I suggest having your AWS contact
181screenshot the results directly within their CloudShell or AWS CLI session. This
182allows you to prove that the data was not modified after the script was run.
183
184* Common Exceptions and False Positives
185
186- *No policy defined*: If ~gather_policy.sh~ exits with a ~NoSuchEntity~ error, the
187 account has no IAM password policy configured. If you were expecting a
188 password policy, document it as a missing control.
189- *HardExpiry: false*: This setting controls whether users are locked out
190 immediately when their password expires or given a grace period to change it.
191 ~false~ is often intentional to avoid lockouts. Check the organization's policy
192 before calling it a finding. Additionally, check if the organization has
193 security exceptions in place before noting a deficiency.
194- *MaxPasswordAge and forced rotation*: A 90-day rotation requirement is common in
195 older policies and frameworks like CIS. NIST 800-63B no longer recommends
196 forced rotation unless there's evidence of compromise. Know which framework
197 you're auditing against before writing up a finding for this setting. Confirm
198 with the organization to understand which framework they used to write their
199 policy.
200- *PasswordReusePrevention*: AWS allows a maximum of 24 previous passwords. If
201 your organization's policy requires a higher number than AWS supports,
202 document the platform limitation rather than raising it as a deficiency.
203
204* How to Write Up the Finding
205
206If a setting fails, here's how to frame it:
207
208- *Deficiency:* The ~MinimumPasswordLength~ setting in the AWS IAM password policy
209 is configured to ~6~, which is below the organization's requirement of ~8~
210 characters.
211- *Root Cause:* Due to {{ root cause }}, the policy was configured to enforce a
212 ~MinimumPasswordLength~ of ~6~.
213- *Risk:* Shorter passwords are more susceptible to brute-force and credential
214 stuffing attacks, increasing the likelihood of unauthorized access to the AWS
215 console.
216- *Evidence:* Refer to ~policy_audit_<timestamp>.csv~ for documentation of testing.
217
218The same structure applies to any other failing rule. For boolean settings, the
219deficiency is simply that the actual value does not match the expected value.
220For numeric settings, the deficiency is that the actual value falls below the
221required minimum.