Introduction

Security is a critical aspect of managing AWS resources. As infrastructure grows, it becomes increasingly challenging to maintain visibility into potential security issues across your AWS environment. This blog introduces a comprehensive Bash script designed to automate the process of identifying common security vulnerabilities in your AWS account.

The script checks for several critical security concerns including public S3 buckets, IAM users without MFA, overly permissive policies, and exposed security groups. Let's dive into the script and understand how each section works.

The script can be accessed here: GitHub

Script Initialization

script_initialization.sh
#!/bin/bash

echo "========================="
echo " AWS Security Audit Script "
echo "========================="

The script begins with a shebang line specifying that it should be executed using Bash. This is followed by a simple header that provides visual clarity when the script is run.

1. Checking for Public S3 Buckets

check_public_s3_buckets.sh
# Function to check for public S3 buckets
check_public_s3_buckets() {
    echo "Checking for public S3 buckets..."
    buckets=$(aws s3 ls | awk '{print $3}')
    for bucket in $buckets; do
        acl=$(aws s3api get-bucket-acl --bucket $bucket --query "Grants[*].Grantee.URI" --output text 2>/dev/null)
        policy=$(aws s3api get-bucket-policy-status --bucket $bucket --query "PolicyStatus.IsPublic" --output text 2>/dev/null)
        
        if [[ "$acl" == *"AllUsers"* ]] || [[ "$acl" == *"AuthenticatedUsers"* ]] || [[ $policy == "True" ]]; then
            echo "⚠️  Public S3 Bucket Found: $bucket"
        fi
    done
}

This function identifies S3 buckets that are publicly accessible, which can be a major security risk. Here's how it works:

  1. First, the script lists all S3 buckets in the account using aws s3 ls and extracts the bucket names with awk.
  2. For each bucket, it checks two aspects of public access:
    • The bucket's ACL (Access Control List) via get-bucket-acl to check if it grants access to "AllUsers" or "AuthenticatedUsers"
    • The bucket's policy via get-bucket-policy-status to check if the policy makes the bucket publicly accessible
  3. If any public access is detected, it alerts the user with a warning emoji and the bucket name.

The 2>/dev/null redirects any error messages to /dev/null to keep the output clean, as not all buckets might have policies.

2. Identifying IAM Users Without MFA

check_iam_users_without_mfa.sh
# Function to check for IAM users without MFA
check_iam_users_without_mfa() {
    echo "Checking IAM users without MFA..."
    users=$(aws iam list-users --query "Users[*].UserName" --output text)
    for user in $users; do
        mfa=$(aws iam list-mfa-devices --user-name $user --query "MFADevices" --output text)
        if [ -z "$mfa" ]; then
            echo "⚠️  User without MFA: $user"
        fi
    done
}

Multi-Factor Authentication (MFA) is a critical security control for AWS IAM users. This function identifies users who don't have MFA enabled:

  1. It lists all IAM users in the account using the aws iam list-users command with a query to extract just the usernames.
  2. For each user, it checks if any MFA devices are associated with the account using aws iam list-mfa-devices.
  3. If the result is empty (using -z to check if the string is zero length), it means the user doesn't have MFA enabled, and the script reports this as a security issue.

Using MFA adds an extra layer of security by requiring users to provide a dynamic token in addition to their password, significantly reducing the risk of unauthorized access even if passwords are compromised.

3. Detecting Overly Permissive IAM Policies

check_overly_permissive_policies.sh
# Function to detect overly permissive IAM policies
check_overly_permissive_policies() {
    echo "Checking for overly permissive IAM policies..."
    policies=$(aws iam list-policies --scope Local --query "Policies[*].Arn" --output text)
    for policy in $policies; do
        doc=$(aws iam get-policy-version --policy-arn $policy --version-id v1 --query "PolicyVersion.Document" --output json 2>/dev/null)
        if [[ "$doc" == *"\"Effect\": \"Allow\""* ]] && [[ "$doc" == *"\"Action\": \"*\""* ]] && [[ "$doc" == *"\"Resource\": \"*\""* ]]; then
            echo "⚠️  Overly Permissive Policy: $policy"
        fi
    done
}

This function detects IAM policies that are overly permissive, specifically looking for the dangerous combination of allowing all actions on all resources:

  1. It retrieves all customer-managed policies (scope: Local) using aws iam list-policies.
  2. For each policy, it fetches the policy document (version v1) using aws iam get-policy-version.
  3. It then checks if the policy document contains the dangerous trifecta:
    • "Effect": "Allow" - The policy is allowing (not denying) actions
    • "Action": "*" - The policy allows all actions
    • "Resource": "*" - The policy applies to all resources
  4. If all three conditions are met, it flags the policy as overly permissive.

Such wildcard policies violate the principle of least privilege and can lead to serious security vulnerabilities by granting users or roles excessive permissions.

4. Checking for Open Security Groups

check_open_security_groups.sh
# Function to check for open security groups
check_open_security_groups() {
    echo "Checking for open security groups..."
    open_sgs=$(aws ec2 describe-security-groups --query "SecurityGroups[?IpPermissions[?contains(IpRanges[].CidrIp, '0.0.0.0/0') || contains(Ipv6Ranges[].CidrIpv6, '::/0')]].GroupId" --output text)
    if [ -n "$open_sgs" ]; then
        echo "⚠️  Security groups allowing unrestricted access: $open_sgs"
    else
        echo "✅  No open security groups found."
    fi
}

Security groups act as a virtual firewall for your AWS resources. This function identifies security groups that allow unrestricted access from the internet:

  1. It uses aws ec2 describe-security-groups with a complex JMESPath query to find security groups that:
    • Have inbound rules (IpPermissions) that allow access from 0.0.0.0/0 (all IPv4 addresses) or
    • Allow access from ::/0 (all IPv6 addresses)
  2. If any such security groups are found, it reports them as a security risk.
  3. If no open security groups are found, it reports a positive confirmation.

Open security groups that allow unrestricted access from the internet are a common security vulnerability that can expose resources to unauthorized access.

5. Finding Inactive IAM Users

check_inactive_iam_users.sh
# Function to find inactive IAM users (Last 90 days)
check_inactive_iam_users() {
    echo "Checking for inactive IAM users..."
    aws iam generate-credential-report
    sleep 5
    inactive_users=$(aws iam get-credential-report --query "Content" --output text | base64 --decode | awk -F, '{if ($7 == "N/A" && $8 == "N/A" && $9 == "N/A" && NR>1) print $1}')
    if [ -n "$inactive_users" ]; then
        echo "⚠️  Inactive IAM Users: $inactive_users"
    else
        echo "✅  No inactive users found."
    fi
}

Inactive IAM users pose a security risk as they may no longer be needed but still retain access to your AWS account. This function identifies these users:

  1. First, it generates a credential report using aws iam generate-credential-report and waits 5 seconds for it to complete.
  2. Then, it retrieves the report with aws iam get-credential-report, which returns base64 encoded content.
  3. The report is decoded and processed with awk to find users where:
    • Column 7 ($7) is "N/A" (password_last_used)
    • Column 8 ($8) is "N/A" (access_key_1_last_used_date)
    • Column 9 ($9) is "N/A" (access_key_2_last_used_date)
    • Row number is greater than 1 (NR>1) to skip the header row
  4. This effectively finds users who have never used their password or access keys.

6. Verifying CloudTrail Logging

check_cloudtrail_status.sh
# Function to check CloudTrail logging status
check_cloudtrail_status() {
    echo "Checking CloudTrail logging status..."
    status=$(aws cloudtrail describe-trails --query "trailList[*].IsLogging" --output text)
    if [ "$status" != "True" ]; then
        echo "⚠️  CloudTrail is not enabled!"
    else
        echo "✅  CloudTrail is enabled."
    fi
}

AWS CloudTrail provides essential audit logs for your AWS account. This function verifies if CloudTrail logging is enabled:

  1. It uses aws cloudtrail describe-trails with a query to extract the logging status of all trails.
  2. If the status is not "True", it alerts that CloudTrail is not enabled.
  3. Otherwise, it confirms that CloudTrail is properly enabled.

CloudTrail is crucial for security monitoring, compliance, and troubleshooting as it records all API calls made in your AWS account.

7. Checking EC2 Instances Without IMDSv2

check_ec2_imds_v2.sh
# Function to check EC2 instances without IMDSv2
check_ec2_imds_v2() {
    echo "Checking EC2 instances without IMDSv2..."
    imds_v1_instances_id=$(aws ec2 describe-instances --query "Reservations[*].Instances[?MetadataOptions.HttpTokens=='optional'].InstanceId" --output text)
    imds_v1_instances_name=$(aws ec2 describe-instances --query "Reservations[*].Instances[?MetadataOptions.HttpTokens=='optional'].KeyName" --output text)
    
    if [ -n "$imds_v1_instances_id" ]; then
        echo "⚠️  EC2 instances without IMDSv2: InstanceId: $imds_v1_instances_id Name:$imds_v1_instances_name"
    else
        echo "✅  All EC2 instances use IMDSv2."
    fi
}

Instance Metadata Service Version 2 (IMDSv2) provides enhanced security for EC2 instances. This function identifies instances still using the less secure IMDSv1:

  1. It uses aws ec2 describe-instances with queries to find instances where HttpTokens is set to 'optional', which means the instance allows IMDSv1 requests.
  2. The script retrieves both the instance IDs and their key names (if any) for identification.
  3. If any instances are found still using IMDSv1, it reports them as a security concern.

IMDSv2 helps prevent server-side request forgery (SSRF) attacks by requiring session authentication tokens, making it significantly more secure than IMDSv1.

Running All Checks

main.sh
# Run all checks
check_public_s3_buckets
check_iam_users_without_mfa
check_overly_permissive_policies
check_open_security_groups
check_inactive_iam_users
check_cloudtrail_status
check_ec2_imds_v2

echo "========================="
echo " AWS Security Audit Completed "
echo "========================="

The final section of the script calls each function sequentially to run all security checks and then displays a completion message.

Conclusion

This AWS Security Audit Script provides a valuable starting point for identifying common security vulnerabilities in your AWS environment. By automating these checks, you can regularly monitor your AWS security posture without manual effort.

For a more comprehensive security approach, consider additionally:

  • Implementing the script as part of a scheduled job or CI/CD pipeline
  • Expanding the script to check for other security best practices
  • Integrating with notification systems like SNS or Slack for real-time alerts
  • Using AWS Config or Security Hub for continuous compliance monitoring

Regular security auditing is essential for maintaining a secure AWS environment. This script helps you take a proactive approach to identifying and addressing potential security issues before they can be exploited.