aboutsummaryrefslogtreecommitdiff
path: root/content/blog/2026-02-21-auditing-aws-passwords.org
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-02-21 21:32:49 -0600
committerGitHub <[email protected]>2026-02-21 21:32:49 -0600
commit8386d675f596e316520655a37d367e37eb7286a5 (patch)
treea8184fd3662b7af68087e7d008fa559f6366e9a3 /content/blog/2026-02-21-auditing-aws-passwords.org
parent3e1b8ee2e31e4bc70441b35647e928834a339f74 (diff)
downloadcleberg.net-8386d675f596e316520655a37d367e37eb7286a5.tar.gz
cleberg.net-8386d675f596e316520655a37d367e37eb7286a5.tar.bz2
cleberg.net-8386d675f596e316520655a37d367e37eb7286a5.zip
publish new post: auditing-aws-passwords (#10)
* clean up latest post for ease of reading * publish new post: auditing-aws-passwords
Diffstat (limited to 'content/blog/2026-02-21-auditing-aws-passwords.org')
-rw-r--r--content/blog/2026-02-21-auditing-aws-passwords.org212
1 files changed, 212 insertions, 0 deletions
diff --git a/content/blog/2026-02-21-auditing-aws-passwords.org b/content/blog/2026-02-21-auditing-aws-passwords.org
new file mode 100644
index 0000000..445732a
--- /dev/null
+++ b/content/blog/2026-02-21-auditing-aws-passwords.org
@@ -0,0 +1,212 @@
+#+date: [2026-02-21 Sat 21:13:45]
+#+title: Auditind AWS Passwords
+#+description: Learn how to audit passwords in AWS.
+#+slug: auditing-aws-passwords
+
+One of the first controls an IT Auditor learns is how to audit passwords in any
+number of IT systems. However, things have changed with the introduction of
+cloud platforms. This post covers the process and results of auditing AWS
+passwords.
+
+The scripts are available at [[https://github.com/audit-labs/audit-tools/tree/main/applications/aws/aws_password_policy][audit-labs/audit-tools]].
+
+* Scoping
+
+First thing's first: scoping. To audit AWS passwords, we need to understand that
+AWS IAM password policies only apply to users with console access. This isn't
+the same as the password policy for an application built on top of AWS. It also
+has no effect on users who authenticate through IAM Identity Center, which
+delegates authentication to an external identity provider, such as Okta or
+Active Directory.
+
+If the organization uses Identity Center exclusively and has no IAM users with
+passwords, you will need to perform a different procedure.
+
+If they do have IAM users with console access, the script applies and is worth
+testing.
+
+* What the Scripts Do
+
+The process runs in two steps:
+1. ~gather_policy.sh~ calls the AWS CLI to fetch the current IAM password policy,
+captures metadata (timestamp, AWS account, region, caller identity), and writes
+everything to a JSON file. This file is evidence of the policy's current state.
+2. ~evaluate_policy.py~ reads that JSON file, prompts you for the expected value of
+each setting, and exports a CSV report with a value of ~PASS~ or ~FAIL~ for each
+rule.
+
+* Prerequisites
+
+To run this script, you'll need:
+
+- Access to CloudShell or the AWS CLI utility installed and configured with
+ credentials that have read access to ~iam:GetAccountPasswordPolicy~ and
+ ~sts:GetCallerIdentity~
+- ~jq~ installed (used by the Bash script to merge JSON objects)
+- Python 3 installed
+- Optional: ~uv~ installed (used to run the Python script)
+
+* Step 1: Gather the Policy
+
+Run the first script to pull the current policy from AWS:
+
+#+begin_src bash
+chmod +x gather_policy.sh
+./gather_policy.sh
+#+end_src
+
+By default, this writes the output to ~policy_report.json~ in the current
+directory. You can specify a custom path with the ~-o~ flag:
+
+#+begin_src bash
+./gather_policy.sh -o /tmp/my_report.json
+#+end_src
+
+The output is a JSON file with two top-level keys: ~metadata~ and ~PasswordPolicy~.
+
+#+begin_src json
+{
+ "metadata": {
+ "report_timestamp_utc": "2025-12-15T01:29:52Z",
+ "os_user": "cloudshell-user",
+ "hostname": "",
+ "working_directory": "/home/cloudshell-user",
+ "aws_profile": "default",
+ "aws_region": "eu-west-1",
+ "aws_caller_identity": {
+ "UserId": "214941490075",
+ "Account": "214941490075",
+ "Arn": "arn:aws:iam::214941490075:root"
+ }
+ },
+ "PasswordPolicy": {
+ "MinimumPasswordLength": 8,
+ "RequireSymbols": true,
+ "RequireNumbers": true,
+ "RequireUppercaseCharacters": true,
+ "RequireLowercaseCharacters": true,
+ "AllowUsersToChangePassword": true,
+ "ExpirePasswords": true,
+ "MaxPasswordAge": 90,
+ "PasswordReusePrevention": 4,
+ "HardExpiry": false
+ }
+}
+#+end_src
+
+The metadata block is what ties this evidence to a specific account and point in
+time. The ~aws_caller_identity~ field shows who ran the script and in which
+account.
+
+* Step 2: Evaluate the Policy
+
+Pass the JSON file to the evaluation script:
+
+#+begin_src bash
+uv run evaluate_policy.py policy_report.json
+#+end_src
+
+The script will prompt you for each of the ten settings. Press ~Enter~ to skip any
+setting you don't need to test. For numeric settings like ~MinimumPasswordLength~
+and ~MaxPasswordAge~, the script treats your input as a minimum, so the actual
+value must be greater than or equal to your expected value to pass. Boolean
+settings require an exact match.
+
+#+begin_src text
+=== Expected / Minimum Values (press <Enter> for N/A) ===
+
+Enter expected value for 'Minimum password length' (int) or press <Enter> to skip: 8
+Enter expected value for 'Require symbols (!@#$...)' (bool) or press <Enter> to skip: true
+Enter expected value for 'Require numbers (0-9)' (bool) or press <Enter> to skip: true
+Enter expected value for 'Require uppercase letters (A-Z)' (bool) or press <Enter> to skip: true
+Enter expected value for 'Require lowercase letters (a-z)' (bool) or press <Enter> to skip: true
+Enter expected value for 'Allow users to change password' (bool) or press <Enter> to skip: true
+Enter expected value for 'Expire passwords (enable aging)' (bool) or press <Enter> to skip: true
+Enter expected value for 'Maximum password age (days)' (int) or press <Enter> to skip: 90
+Enter expected value for 'Prevent password reuse (last N)' (int) or press <Enter> to skip: 4
+Enter expected value for 'Hard expiry (no grace period)' (bool) or press <Enter> to skip: false
+
+Audit CSV written to: policy_audit_20251215T014323Z.csv
+
+Summary:
+ 1. Minimum password length -> PASS
+ 2. Require symbols (!@#$...) -> PASS
+ 3. Require numbers (0-9) -> PASS
+ 4. Require uppercase letters (A-Z) -> PASS
+ 5. Require lowercase letters (a-z) -> PASS
+ 6. Allow users to change password -> PASS
+ 7. Expire passwords (enable aging) -> PASS
+ 8. Maximum password age (days) -> PASS
+ 9. Prevent password reuse (last N) -> PASS
+ 10. Hard expiry (no grace period) -> PASS
+
+--- End of report ---
+#+end_src
+
+* Reading the CSV
+
+The CSV is your evidence. It includes the metadata header from the JSON file, so
+the account ID, timestamp, and caller identity are embedded directly in the
+file.
+
+#+begin_src csv
+# report_timestamp_utc: 2025-12-15T01:29:52Z
+# os_user: cloudshell-user
+# hostname:
+# working_directory: /home/cloudshell-user
+# aws_profile: default
+# aws_region: eu-west-1
+# aws_caller_identity: {'UserId': '214941490075', 'Account': '214941490075', 'Arn': 'arn:aws:iam::214941490075:root'}
+
+Rule#,Policy-Item,Expected,Actual,Result
+1,Minimum password length,8,8,PASS
+2,Require symbols (!@#$...),true,true,PASS
+3,Require numbers (0-9),true,true,PASS
+4,Require uppercase letters (A-Z),true,true,PASS
+5,Require lowercase letters (a-z),true,true,PASS
+6,Allow users to change password,true,true,PASS
+7,Expire passwords (enable aging),true,true,PASS
+8,Maximum password age (days),90,90,PASS
+9,Prevent password reuse (last N),4,4,PASS
+10,Hard expiry (no grace period),false,false,PASS
+#+end_src
+
+You may use this evidence in any form, but I suggest having your AWS contact
+screenshot the results directly within their CloudShell or AWS CLI session. This
+allows you to prove that the data was not modified after the script was run.
+
+* Common Exceptions and False Positives
+
+- *No policy defined*: If ~gather_policy.sh~ exits with a ~NoSuchEntity~ error, the
+account has no IAM password policy configured. If you were expecting a password
+policy, document it as a missing control.
+- *HardExpiry: false*: This setting controls whether users are locked out
+immediately when their password expires or given a grace period to change it.
+~false~ is often intentional to avoid lockouts. Check the organization's policy
+before calling it a finding. Additionally, check if the organization has
+security exceptions in place before noting a deficiency.
+- *MaxPasswordAge and forced rotation*: A 90-day rotation requirement is common in
+older policies and frameworks like CIS. NIST 800-63B no longer recommends forced
+rotation unless there's evidence of compromise. Know which framework you're
+auditing against before writing up a finding for this setting. Confirm with the
+organization to understand which framework they used to write their policy.
+- *PasswordReusePrevention*: AWS allows a maximum of 24 previous passwords. If your
+organization's policy requires a higher number than AWS supports, document the
+platform limitation rather than raising it as a deficiency.
+
+* How to Write Up the Finding
+
+If a setting fails, here's how to frame it:
+- *Deficiency:* The ~MinimumPasswordLength~ setting in the AWS IAM password policy is
+configured to ~6~, which is below the organization's requirement of ~8~ characters.
+- *Root Cause:* Due to {{ root cause }}, the policy was configured to enforce a
+ ~MinimumPasswordLength~ of ~6~.
+- *Risk:* Shorter passwords are more susceptible to brute-force and credential
+stuffing attacks, increasing the likelihood of unauthorized access to the AWS
+console.
+- *Evidence:* Refer to ~policy_audit_<timestamp>.csv~ for documentation of testing.
+
+The same structure applies to any other failing rule. For boolean settings, the
+deficiency is simply that the actual value does not match the expected value.
+For numeric settings, the deficiency is that the actual value falls below the
+required minimum.