diff options
| author | Christian Cleberg <[email protected]> | 2026-02-21 21:32:49 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-21 21:32:49 -0600 |
| commit | 8386d675f596e316520655a37d367e37eb7286a5 (patch) | |
| tree | a8184fd3662b7af68087e7d008fa559f6366e9a3 /content/blog/2026-02-21-auditing-aws-passwords.org | |
| parent | 3e1b8ee2e31e4bc70441b35647e928834a339f74 (diff) | |
| download | cleberg.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.org | 212 |
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. |
