Commit 8386d675f5

8386d675f596e316520655a37d367e37eb7286a5

parent: 3e1b8ee2e3

Unregistered key

cmc <hello@cleberg.net> · 2026-02-22 03:32 UTC
committer: <noreply@github.com>

publish new post: auditing-aws-passwords (#10)

* clean up latest post for ease of reading

* publish new post: auditing-aws-passwords

Layout: unified · split

content/blog/2026-02-21-auditing-aws-iam.org +106 −132
@@ -1,44 +1,50 @@
1#+date: [2026-02-21 Sat 18:58:39] 1#+date: [2026-02-21 Sat 18:58:39]
2#+title: Auditing AWS IAM Identity Center Assignments 2#+title: Auditing AWS IAM Identity Center Assignments
3#+description: How to audit IAM Identity Center access assignments in AWS using a shell script, including what to look for, common exceptions, and how to write up findings. 3#+description: Learn how to audit IAM user assignments in AWS.
4#+slug: auditing-aws-iam 4#+slug: auditing-aws-iam
5 5
6If you've ever asked an IT team for a list of who has access to an AWS account, 6If you've ever asked an IT team for a list of who has access to an AWS account,
7you've probably received a spreadsheet that was already out of date. Or worse, 7you've probably received a spreadsheet that was already out of date. Or worse,
8an out-of-context screenshot that doesn't explain anything. IAM Identity Center 8an out-of-context screenshot that doesn't explain anything. Or worse, they don't
9doesn't make this easy to extract through the console, and most teams don't have 9know either and ignore you, hoping you forget about the request (ask me how I
10a repeatable process for pulling it. This post walks through a script that does 10know!).
11it for you and explains what to look for once you have the output. 11
12To make it worse, IAM Identity Center doesn't make this easy to extract through
13the console, and most teams don't have a repeatable process for pulling it.
14
15To solve this problem, this post walks through a script that does it for you and
16explains what to look for once you have the output. This is something I've used
17in practice and want to share, as cloud auditing knowledge seems scarce.
12 18
13The script is available at [[https://github.com/audit-labs/audit-tools/blob/main/applications/aws/aws_iam_users.sh][audit-labs/audit-tools]]. 19The script is available at [[https://github.com/audit-labs/audit-tools/blob/main/applications/aws/aws_iam_users.sh][audit-labs/audit-tools]].
14 20
15* Background: How AWS Provisions Access 21* Background: How AWS Provisions Access
16 22
17Before running anything, it helps to understand the different ways AWS can be 23Before running anything, it helps to understand the different ways AWS can be
18architected to grant access. You may encounter any of these in practice, 24architected to grant access. You may encounter any one (or all) of these in
19sometimes within the same organization, and knowing how they differ tells you 25practice, sometimes within the same organization. Knowing how they differ tells
20where to look during an audit and which questions to ask. 26you where to look during an audit and which questions to ask.
21 27
22** Traditional IAM Users 28** IAM Users
23 29
24The original AWS access model. An IAM user is an identity created directly 30This is the original AWS access model. An IAM user is an identity created
25inside a single AWS account. It has a username, optional console password, and 31directly inside a single AWS account (e.g., ~iamuser1~ created inside AWS user
32~cmc~). This type of account has a username, an optional console password, and
26optional access keys for programmatic access. Permissions are granted by 33optional access keys for programmatic access. Permissions are granted by
27attaching IAM policies directly to the user, or by adding the user to an IAM 34attaching IAM policies directly to the user, or by adding the user to an IAM
28group that has policies attached to it. 35group that has policies attached to it.
29 36
30You'll still see IAM users in older environments, smaller organizations, and in
31cases where a service or application needs long-lived programmatic credentials.
32The core problem with IAM users at scale is that they're account-scoped, meaning 37The core problem with IAM users at scale is that they're account-scoped, meaning
33a user in ~Account A~ has no relationship to a user in ~Account B~, even if they're 38a user in ~Account A~ has no relationship to a user in ~Account B~, even if they're
34the same person. Organizations with dozens or hundreds of accounts end up with a 39the same person. Organizations with dozens or hundreds of accounts end up with a
35fragmented identity landscape that's difficult to review and even harder to 40fragmented identity landscape that's difficult to review and even harder to
36deprovision consistently. 41deprovision consistently.
37 42
38From an audit perspective, IAM users are high-risk when they have active access 43From an audit perspective, these are key risks to understand during your
39keys (long-lived credentials that don't expire unless explicitly rotated or 44walkthrough of the environment. Additionally, you should note that IAM users are
40revoked) or when policies are attached directly to the user rather than 45high-risk when they have active access keys (long-lived credentials that don't
41inherited through a group. 46expire unless explicitly rotated or revoked) or when policies are attached
47directly to the user rather than inherited through a group.
42 48
43** IAM Groups 49** IAM Groups
44 50
@@ -86,30 +92,31 @@ Policies are the documents that define permissions. They're not an access
86pattern on their own, but understanding the three types helps when reviewing 92pattern on their own, but understanding the three types helps when reviewing
87output from any access audit: 93output from any access audit:
88 94
89- *AWS managed policies* are created and maintained by AWS. Examples include 951. *AWS managed policies* are created and maintained by AWS. Examples include
90 ~AdministratorAccess~, ~ReadOnlyAccess~, and job-function policies like ~Billing~. 96 ~AdministratorAccess~, ~ReadOnlyAccess~, and job-function policies like ~Billing~.
91 They're convenient but broad: ~AdministratorAccess~ is a single policy that 97 They're convenient but broad: ~AdministratorAccess~ is a single policy that
92 grants full access to every AWS service. 98 grants full access to every AWS service.
93- *Customer managed policies* are created by the organization. They offer more 992. *Customer managed policies* are created by the organization. They offer more
94 precise control and can be reused across users, groups, and roles. Better 100 precise control and can be reused across users, groups, and roles. Better
95 practice than relying entirely on AWS managed policies for anything sensitive. 101 practice than relying entirely on AWS managed policies for anything
96- *Inline policies* are embedded directly into a specific user, group, or role 102 sensitive.
97 rather than existing as a standalone resource. They can't be reused and are 1033. *Inline policies* are embedded directly into a specific user, group, or role
98 easier to miss during reviews. When you see an inline policy in audit output, 104 rather than existing as a standalone resource. They can't be reused and are
99 treat it as something that warrants a closer look — they're often added ad hoc 105 easier to miss during reviews. When you see an inline policy in audit output,
100 to solve a specific problem and not always well documented. 106 treat it as something that warrants a closer look — they're often added ad
107 hoc to solve a specific problem and not always well documented.
101 108
102** IAM Identity Center 109** IAM Identity Center
103 110
104IAM Identity Center (formerly AWS SSO) is the current recommended approach for 111IAM Identity Center (formerly AWS SSO) is the current recommended approach for
105human access in multi-account AWS Organizations. It sits above individual 112human (end-user) access in multi-account AWS Organizations. It sits above
106accounts and provides centralized access management across all of them from a 113individual accounts and provides centralized access management across all of
107single place. 114them from a single place.
108 115
109It connects to an identity source, such as AWS's own directory, Active 116It connects to an identity source, such as AWS's own directory, Active
110Directory, or an external IdP like Okta or Entra ID, and uses that as the source 117Directory, or an externali identity provider (IdP) like Okta or Entra ID, and
111of truth for users and groups. Access is configured within Identity Center 118uses that as the source of truth for users and groups. Access is configured
112itself, not inside individual accounts. 119within Identity Center itself, not inside individual accounts.
113 120
114The key construct is the permission set: a named bundle of IAM policies 121The key construct is the permission set: a named bundle of IAM policies
115(managed, customer managed, and/or inline) that defines what a principal can do. 122(managed, customer managed, and/or inline) that defines what a principal can do.
@@ -135,32 +142,35 @@ why.
135 142
136* What the Script Does 143* What the Script Does
137 144
138The script works against a single named AWS account and produces a JSON report 145The script works by running commands against a single named AWS account and
139of every user and group assignment, along with the permission set and policies 146produces a JSON report of every user and group assignment, along with the
140attached to each assignment. 147permission set and policies attached to each assignment.
141 148
142It runs in four steps: 149It runs in four steps:
143 150
1441. Finds the IAM Identity Center instance and looks up the target account ID by 1511. Finds the IAM Identity Center instance and looks up the target account ID by
145 name 152 name;
1462. Lists all permission sets provisioned to that account 1532. Lists all permission sets provisioned to that account;
1473. For each permission set, retrieves all user and group assignments, resolves 1543. For each permission set, retrieves all user and group assignments, resolves
148 their names from the Identity Store, and fetches the managed and inline 155 their names from the Identity Store, and fetches the managed and inline
149 policies attached to the permission set 156 policies attached to the permission set;
1504. Writes everything to a JSON file named ~report_<account_name>.json~ 1574. Writes everything to a JSON file named ~report_<account_name>.json~.
151 158
152The script caches permission set details and principal names to avoid redundant 159The script caches permission set details and principal names to avoid redundant
153API calls when the same permission set or user appears in multiple assignments. 160API calls when the same permission set or user appears in multiple assignments.
154 161
162At the end of this process, we will have the evidence we need to test controls
163such as administrative access or segregation of duties.
164
155* Prerequisites 165* Prerequisites
156 166
157You'll need: 167You'll need:
158 168
159- AWS CLI installed and configured with credentials that have read access to 169- AWS CLI (or AWS CloudShell) installed and configured with credentials that
160 ~sso-admin~, ~identitystore~, and ~organizations~ 170 have read access to ~sso-admin~, ~identitystore~, and ~organizations~;
161- ~jq~ installed (used throughout the script for JSON parsing) 171- ~jq~ installed (used throughout the script for JSON parsing);
162- The name of the AWS account you want to audit (must match exactly as it 172- The name of the AWS account you want to audit (must match exactly as it
163 appears in AWS Organizations) 173 appears in AWS Organizations).
164 174
165Set the account name at the top of the script before running: 175Set the account name at the top of the script before running:
166 176
@@ -179,32 +189,6 @@ chmod +x aws_iam_users.sh
179#+attr_html: :alt Terminal output of aws_iam_users.sh showing discovered assignments for the cmc account. 189#+attr_html: :alt Terminal output of aws_iam_users.sh showing discovered assignments for the cmc account.
180[[https://img.cleberg.net/blog/20260221-auditing-aws-iam/script.webp]] 190[[https://img.cleberg.net/blog/20260221-auditing-aws-iam/script.webp]]
181 191
182
183* Key Snippet: Resolving Principal Names
184
185The part of the script most worth understanding is how it resolves user and
186group names. AWS stores principals in Identity Center by ID, not by name. To get
187a readable name, the script calls the Identity Store API separately for each
188principal:
189
190#+begin_src bash
191if [ "$PRINCIPAL_TYPE" == "USER" ]; then
192 PRINCIPAL_NAME=$(aws identitystore describe-user \
193 --identity-store-id "$IDENTITY_STORE_ID" \
194 --user-id "$PRINCIPAL_ID" \
195 --query "UserName" --output text 2>/dev/null)
196elif [ "$PRINCIPAL_TYPE" == "GROUP" ]; then
197 PRINCIPAL_NAME=$(aws identitystore describe-group \
198 --identity-store-id "$IDENTITY_STORE_ID" \
199 --group-id "$PRINCIPAL_ID" \
200 --query "DisplayName" --output text 2>/dev/null)
201fi
202#+end_src
203
204This is also where the cache matters. If the same user appears across multiple
205permission set assignments (which is common), the script reuses the stored name
206rather than making a redundant API call.
207
208* Reading the Output 192* Reading the Output
209 193
210The script produces a JSON array where each element represents a single 194The script produces a JSON array where each element represents a single
@@ -266,74 +250,64 @@ principal-to-permission-set assignment. Here's an example:
266 250
267A few things to look for when reviewing this output: 251A few things to look for when reviewing this output:
268 252
269*Direct user assignments*: ~iamtestuser1~ is assigned ~AdministratorAccess~ directly 253- *Direct user assignments*: ~iamtestuser1~ is assigned ~AdministratorAccess~ directly
270as a USER, not through a group. Access should generally be provisioned through 254 as a USER, not through a group. Access should generally be provisioned through
271groups so that provisioning and deprovisioning are tied to group membership, not 255 groups so that provisioning and deprovisioning are tied to group membership,
272managed case-by-case. A direct user assignment to a high-privilege permission 256 not managed case-by-case. A direct user assignment to a high-privilege
273set warrants a conversation with IT about why it exists. 257 permission set warrants a conversation with IT about why it exists.
274 258- *AdministratorAccess*: This is AWS's broadest managed policy, which allows full
275*AdministratorAccess*: This is AWS's broadest managed policy, which allows full 259 access to everything in the account. Note who has it and whether that's
276access to everything in the account. Note who has it and whether that's 260 appropriate. In many environments, even senior engineers shouldn't have
277appropriate. In many environments, even senior engineers shouldn't have standing 261 standing ~AdministratorAccess~. It should be reserved for break-glass accounts
278~AdministratorAccess~; it should be reserved for break-glass accounts or 262 or infrastructure teams with a documented need.
279infrastructure teams with a documented need. 263- *Inline policies*: The ~inline_policy~ field in this example contains a Deny
280 264 statement for ~a4b:*~. Inline policies attached to permission sets are worth
281*Inline policies*: The ~inline_policy~ field in this example contains a Deny 265 reviewing, as they can add permissions or restrict them, and they're easy to
282statement for ~a4b:*~ (Alexa for Business). Inline policies attached to permission 266 miss if you're only looking at the managed policy name.
283sets are worth reviewing, as they can add permissions or restrict them, and 267- *Multiple assignments for the same user*: ~iamtestuser1~ appears twice: once with
284they're easy to miss if you're only looking at the managed policy name. 268 ~AdministratorAccess~ and once with ~Billing~. This isn't automatically a problem,
285 269 but a user with both full administrative access and explicit billing access is
286*Multiple assignments for the same user*: ~iamtestuser1~ appears twice: once with 270 worth confirming with IT.
287~AdministratorAccess~ and once with ~Billing~. This isn't automatically a problem,
288but a user with both full administrative access and explicit billing access is
289worth confirming with IT.
290 271
291* Common Exceptions and False Positives 272* Common Exceptions and False Positives
292 273
293*Break-glass accounts*: Most organizations maintain at least one emergency account 274- *Break-glass accounts*: Most organizations maintain at least one emergency
294with direct ~AdministratorAccess~ that bypasses normal access controls. A direct 275 account with direct ~AdministratorAccess~ that bypasses normal access controls.
295USER assignment to ~AdministratorAccess~ may be intentional for this reason. Ask 276 A direct USER assignment to ~AdministratorAccess~ may be intentional for this
296IT whether a break-glass account exists, confirm the username matches, and 277 reason. Ask IT whether a break-glass account exists, confirm the username
297document it as an accepted exception if appropriate. 278 matches, and document it as an accepted exception if appropriate.
298 279- *Small organizations or early-stage environments*: Some teams haven't
299*Small organizations or early-stage environments*: Some teams haven't implemented 280 implemented group-based access management yet and assign permissions directly
300group-based access management yet and assign permissions directly to users 281 to users across the board. This is a control gap, but the finding should
301across the board. This isn't a false positive, it's a control gap, but the 282 reflect the scale and maturity of the environment. A two-person startup with
302finding should reflect the scale and maturity of the environment. A two-person 283 direct assignments is a different risk than an enterprise doing the same
303startup with direct assignments is a different risk than an enterprise doing the 284 thing. This may or may not give rise to a deficiency.
304same thing. 285- *Service or automation accounts*: Occasionally a ~USER~ principal in Identity
305 286 Center is a service account rather than a human. These may legitimately have
306*Service or automation accounts*: Occasionally a USER principal in Identity Center 287 specific permission sets assigned directly. Confirm the account's purpose
307is a service account rather than a human. These may legitimately have specific 288 before writing it up.
308permission sets assigned directly. Confirm the account's purpose before writing
309it up.
310 289
311* How to Write Up the Finding 290* How to Write Up the Finding
312 291
313If you identify a direct high-privilege user assignment that isn't a documented 292If you identify a direct high-privilege user assignment that isn't a documented
314exception, here's how to frame it: 293exception, here's how to frame it:
315 294- *Deficiency:* User ~iamtestuser1~ is directly assigned the ~AdministratorAccess~
316*Condition:* User ~iamtestuser1~ is directly assigned the ~AdministratorAccess~ 295 permission set in account ~cmc~, rather than receiving access through a group
317permission set in account ~cmc~, rather than receiving access through a group 296 assignment.
318assignment. 297- *Root Cause:* Due to {{ root cuase }}, the user ~iamtestuser1~ was inappropriately
319 298 provisioned the ~AdministratorAccess~ permission directly rather than through a
320*Criteria:* Access to AWS accounts via IAM Identity Center should be provisioned 299 group assignment.
321through groups to ensure consistent access management. Direct user assignments 300- *Risk:* Direct user assignments increase the likelihood of orphaned access
322bypass group-based controls and are not subject to standard group membership 301 following role changes or departures. They are also harder to identify during
323reviews. 302 periodic access reviews, as reviewers may focus on group membership rather
324 303 than individual assignments.
325*Risk:* Direct user assignments increase the likelihood of orphaned access 304- *Evidence:* Attach the relevant portion of ~report_cmc.json~ filtered to show the
326following role changes or departures. They are also harder to identify during 305 specific principal, permission set, and policies. The JSON output is
327periodic access reviews, as reviewers may focus on group membership rather than 306 self-documenting, as it includes the account name in the filename and contains
328individual assignments. 307 the full policy details alongside the assignment.
329 308
330*Evidence:* Attach the relevant portion of ~report_cmc.json~ filtered to show the 309If requested during review, you can filter the output to just the direct user
331specific principal, permission set, and policies. The JSON output is 310assignments:
332self-documenting, as it includes the account name in the filename and contains
333the full policy details alongside the assignment.
334
335To filter the output to just the direct user assignments for the evidence
336attachment:
337 311
338#+begin_src bash 312#+begin_src bash
339jq '[.[] | select(.principal.type == "USER")]' report_cmc.json 313jq '[.[] | select(.principal.type == "USER")]' report_cmc.json
content/blog/2026-02-21-auditing-aws-passwords.org added +212
@@ -0,0 +1,212 @@
1#+date: [2026-02-21 Sat 21:13:45]
2#+title: Auditind AWS Passwords
3#+description: Learn how to audit passwords in AWS.
4#+slug: auditing-aws-passwords
5
6One of the first controls an IT Auditor learns is how to audit passwords in any
7number of IT systems. However, things have changed with the introduction of
8cloud platforms. This post covers the process and results of auditing AWS
9passwords.
10
11The scripts are available at [[https://github.com/audit-labs/audit-tools/tree/main/applications/aws/aws_password_policy][audit-labs/audit-tools]].
12
13* Scoping
14
15First thing's first: scoping. To audit AWS passwords, we need to understand that
16AWS IAM password policies only apply to users with console access. This isn't
17the same as the password policy for an application built on top of AWS. It also
18has no effect on users who authenticate through IAM Identity Center, which
19delegates authentication to an external identity provider, such as Okta or
20Active Directory.
21
22If the organization uses Identity Center exclusively and has no IAM users with
23passwords, you will need to perform a different procedure.
24
25If they do have IAM users with console access, the script applies and is worth
26testing.
27
28* What the Scripts Do
29
30The process runs in two steps:
311. ~gather_policy.sh~ calls the AWS CLI to fetch the current IAM password policy,
32captures metadata (timestamp, AWS account, region, caller identity), and writes
33everything to a JSON file. This file is evidence of the policy's current state.
342. ~evaluate_policy.py~ reads that JSON file, prompts you for the expected value of
35each setting, and exports a CSV report with a value of ~PASS~ or ~FAIL~ for each
36rule.
37
38* Prerequisites
39
40To run this script, you'll need:
41
42- Access to CloudShell or the AWS CLI utility installed and configured with
43 credentials that have read access to ~iam:GetAccountPasswordPolicy~ and
44 ~sts:GetCallerIdentity~
45- ~jq~ installed (used by the Bash script to merge JSON objects)
46- Python 3 installed
47- Optional: ~uv~ installed (used to run the Python script)
48
49* Step 1: Gather the Policy
50
51Run the first script to pull the current policy from AWS:
52
53#+begin_src bash
54chmod +x gather_policy.sh
55./gather_policy.sh
56#+end_src
57
58By default, this writes the output to ~policy_report.json~ in the current
59directory. You can specify a custom path with the ~-o~ flag:
60
61#+begin_src bash
62./gather_policy.sh -o /tmp/my_report.json
63#+end_src
64
65The output is a JSON file with two top-level keys: ~metadata~ and ~PasswordPolicy~.
66
67#+begin_src json
68{
69 "metadata": {
70 "report_timestamp_utc": "2025-12-15T01:29:52Z",
71 "os_user": "cloudshell-user",
72 "hostname": "",
73 "working_directory": "/home/cloudshell-user",
74 "aws_profile": "default",
75 "aws_region": "eu-west-1",
76 "aws_caller_identity": {
77 "UserId": "214941490075",
78 "Account": "214941490075",
79 "Arn": "arn:aws:iam::214941490075:root"
80 }
81 },
82 "PasswordPolicy": {
83 "MinimumPasswordLength": 8,
84 "RequireSymbols": true,
85 "RequireNumbers": true,
86 "RequireUppercaseCharacters": true,
87 "RequireLowercaseCharacters": true,
88 "AllowUsersToChangePassword": true,
89 "ExpirePasswords": true,
90 "MaxPasswordAge": 90,
91 "PasswordReusePrevention": 4,
92 "HardExpiry": false
93 }
94}
95#+end_src
96
97The metadata block is what ties this evidence to a specific account and point in
98time. The ~aws_caller_identity~ field shows who ran the script and in which
99account.
100
101* Step 2: Evaluate the Policy
102
103Pass the JSON file to the evaluation script:
104
105#+begin_src bash
106uv run evaluate_policy.py policy_report.json
107#+end_src
108
109The script will prompt you for each of the ten settings. Press ~Enter~ to skip any
110setting you don't need to test. For numeric settings like ~MinimumPasswordLength~
111and ~MaxPasswordAge~, the script treats your input as a minimum, so the actual
112value must be greater than or equal to your expected value to pass. Boolean
113settings require an exact match.
114
115#+begin_src text
116=== Expected / Minimum Values (press <Enter> for N/A) ===
117
118Enter expected value for 'Minimum password length' (int) or press <Enter> to skip: 8
119Enter expected value for 'Require symbols (!@#$...)' (bool) or press <Enter> to skip: true
120Enter expected value for 'Require numbers (0-9)' (bool) or press <Enter> to skip: true
121Enter expected value for 'Require uppercase letters (A-Z)' (bool) or press <Enter> to skip: true
122Enter expected value for 'Require lowercase letters (a-z)' (bool) or press <Enter> to skip: true
123Enter expected value for 'Allow users to change password' (bool) or press <Enter> to skip: true
124Enter expected value for 'Expire passwords (enable aging)' (bool) or press <Enter> to skip: true
125Enter expected value for 'Maximum password age (days)' (int) or press <Enter> to skip: 90
126Enter expected value for 'Prevent password reuse (last N)' (int) or press <Enter> to skip: 4
127Enter expected value for 'Hard expiry (no grace period)' (bool) or press <Enter> to skip: false
128
129Audit CSV written to: policy_audit_20251215T014323Z.csv
130
131Summary:
132 1. Minimum password length -> PASS
133 2. Require symbols (!@#$...) -> PASS
134 3. Require numbers (0-9) -> PASS
135 4. Require uppercase letters (A-Z) -> PASS
136 5. Require lowercase letters (a-z) -> PASS
137 6. Allow users to change password -> PASS
138 7. Expire passwords (enable aging) -> PASS
139 8. Maximum password age (days) -> PASS
140 9. Prevent password reuse (last N) -> PASS
141 10. Hard expiry (no grace period) -> PASS
142
143--- End of report ---
144#+end_src
145
146* Reading the CSV
147
148The CSV is your evidence. It includes the metadata header from the JSON file, so
149the account ID, timestamp, and caller identity are embedded directly in the
150file.
151
152#+begin_src csv
153# report_timestamp_utc: 2025-12-15T01:29:52Z
154# os_user: cloudshell-user
155# hostname:
156# working_directory: /home/cloudshell-user
157# aws_profile: default
158# aws_region: eu-west-1
159# aws_caller_identity: {'UserId': '214941490075', 'Account': '214941490075', 'Arn': 'arn:aws:iam::214941490075:root'}
160
161Rule#,Policy-Item,Expected,Actual,Result
1621,Minimum password length,8,8,PASS
1632,Require symbols (!@#$...),true,true,PASS
1643,Require numbers (0-9),true,true,PASS
1654,Require uppercase letters (A-Z),true,true,PASS
1665,Require lowercase letters (a-z),true,true,PASS
1676,Allow users to change password,true,true,PASS
1687,Expire passwords (enable aging),true,true,PASS
1698,Maximum password age (days),90,90,PASS
1709,Prevent password reuse (last N),4,4,PASS
17110,Hard expiry (no grace period),false,false,PASS
172#+end_src
173
174You may use this evidence in any form, but I suggest having your AWS contact
175screenshot the results directly within their CloudShell or AWS CLI session. This
176allows you to prove that the data was not modified after the script was run.
177
178* Common Exceptions and False Positives
179
180- *No policy defined*: If ~gather_policy.sh~ exits with a ~NoSuchEntity~ error, the
181account has no IAM password policy configured. If you were expecting a password
182policy, document it as a missing control.
183- *HardExpiry: false*: This setting controls whether users are locked out
184immediately when their password expires or given a grace period to change it.
185~false~ is often intentional to avoid lockouts. Check the organization's policy
186before calling it a finding. Additionally, check if the organization has
187security exceptions in place before noting a deficiency.
188- *MaxPasswordAge and forced rotation*: A 90-day rotation requirement is common in
189older policies and frameworks like CIS. NIST 800-63B no longer recommends forced
190rotation unless there's evidence of compromise. Know which framework you're
191auditing against before writing up a finding for this setting. Confirm with the
192organization to understand which framework they used to write their policy.
193- *PasswordReusePrevention*: AWS allows a maximum of 24 previous passwords. If your
194organization's policy requires a higher number than AWS supports, document the
195platform limitation rather than raising it as a deficiency.
196
197* How to Write Up the Finding
198
199If a setting fails, here's how to frame it:
200- *Deficiency:* The ~MinimumPasswordLength~ setting in the AWS IAM password policy is
201configured to ~6~, which is below the organization's requirement of ~8~ characters.
202- *Root Cause:* Due to {{ root cause }}, the policy was configured to enforce a
203 ~MinimumPasswordLength~ of ~6~.
204- *Risk:* Shorter passwords are more susceptible to brute-force and credential
205stuffing attacks, increasing the likelihood of unauthorized access to the AWS
206console.
207- *Evidence:* Refer to ~policy_audit_<timestamp>.csv~ for documentation of testing.
208
209The same structure applies to any other failing rule. For boolean settings, the
210deficiency is simply that the actual value does not match the expected value.
211For numeric settings, the deficiency is that the actual value falls below the
212required minimum.