Date
April 20, 2026
Author
Karan Patel
,
CEO

Most AWS security checklists stop at "enable MFA" and "don't use root." That is the baseline, and it barely scratches the surface of what attackers actually exploit in the wild. The gaps that get organizations breached are rarely the obvious ones. They live in IAM trust policies, forgotten Lambda execution roles, publicly exposed metadata endpoints, and S3 bucket policies that someone wrote at 2 a.m. and never revisited.

This post walks through the AWS security checklist that practitioners actually need, the one that covers the misconfigurations buried three levels deep that your last cloud audit probably missed.

Why Standard AWS Checklists Fall Short

The problem with most AWS security guidance is that it is written for compliance, not for adversarial thinking. An attacker approaching your AWS environment is not checking whether CloudTrail is enabled. They are looking for an IAM role with sts:AssumeRole open to *, an EC2 instance profile with more permissions than it needs, or a Lambda function whose environment variables contain hardcoded credentials.

The checklist below is organized around attack surfaces, not compliance checkboxes.

IAM: The Most Abused Attack Surface in AWS

Check for Overly Permissive Trust Policies

IAM trust policies define who can assume a role. A misconfigured trust policy is one of the most common paths to privilege escalation in AWS environments.

Run this to audit all roles and surface any with wildcard principals in their trust policies:

aws iam list-roles --query 'Roles[*].[RoleName,AssumeRolePolicyDocument]' \
 --output json | python3 -c "
import json, sys
roles = json.load(sys.stdin)
for role in roles:
   name = role[0]
   doc = role[1]
   for stmt in doc.get('Statement', []):
       principal = stmt.get('Principal', {})
       if principal == '*' or (isinstance(principal, dict) and '*' in principal.get('AWS', '')):
           print(f'[!] Wildcard principal in role: {name}')
"

[cta]

Any role that allows * as a principal is a critical finding. An attacker with any foothold in the account can assume that role and inherit its permissions.

Enumerate Inline vs Managed Policies for Privilege Escalation Paths

Attackers look for specific IAM permission combinations that allow privilege escalation without needing iam:CreateRole. Tools like Pacu and custom scripts built around the Boto3 SDK are commonly used for this. Here is a manual approach using the AWS CLI to find users with dangerous permission combinations:

# List all users and their attached managed policies
for user in $(aws iam list-users --query 'Users[*].UserName' --output text); do
   echo "=== $user ==="
   aws iam list-attached-user-policies --user-name "$user" \
       --query 'AttachedPolicies[*].PolicyName' --output text
   aws iam list-user-policies --user-name "$user" \
       --query 'PolicyNames' --output text
done

[cta]

If you find a user with iam:PassRole combined with lambda:CreateFunction and lambda:InvokeFunction, that is a textbook privilege escalation path. The user can create a Lambda function, pass it a high-privileged role, invoke the function, and execute arbitrary code under that role's permissions.

Detect Stale Access Keys

Long-lived access keys that have not been rotated are a persistent risk. This command lists all access keys and their last-used dates:

aws iam generate-credential-report && sleep 5
aws iam get-credential-report --query 'Content' --output text \
 | base64 --decode \
 | awk -F',' 'NR>1 && $9 != "N/A" {
     cmd = "date -d \"" $9 "\" +%s 2>/dev/null || date -j -f \"%Y-%m-%dT%H:%M:%S+00:00\" \"" $9 "\" +%s"
     cmd | getline last_used
     close(cmd)
     now = systime()
     days = int((now - last_used) / 86400)
     if (days > 90) print "[STALE] User: " $1 ", Key last used: " $9 ", Days ago: " days
 }'

[cta]

Keys unused for more than 90 days should be disabled immediately and reviewed before deletion. Keys that have never been used are even higher priority for removal.

If you want to understand how these misconfigurations translate into real attack chains, the AWS Pentesting Course at Redfox Cybersecurity Academy walks through IAM privilege escalation scenarios in a hands-on lab environment built around real-world attack patterns.

S3: Beyond "Block Public Access"

Audit Bucket Policies for Cross-Account Access

"Block Public Access" is enabled on most accounts now. What teams miss is the cross-account access embedded in bucket policies, often left in place after a vendor engagement or data-sharing arrangement that ended months ago.

for bucket in $(aws s3api list-buckets --query 'Buckets[*].Name' --output text); do
   policy=$(aws s3api get-bucket-policy --bucket "$bucket" \
       --query 'Policy' --output text 2>/dev/null)
   if [ "$policy" != "None" ] && [ -n "$policy" ]; then
       echo "=== Bucket: $bucket ==="
       echo "$policy" | python3 -c "
import json, sys
policy = json.load(sys.stdin)
for stmt in policy.get('Statement', []):
   principal = stmt.get('Principal', {})
   effect = stmt.get('Effect', '')
   actions = stmt.get('Action', [])
   if effect == 'Allow':
       if isinstance(principal, str) and principal == '*':
           print('[CRITICAL] Public Allow statement found')
       elif isinstance(principal, dict):
           aws_principals = principal.get('AWS', [])
           if isinstance(aws_principals, str):
               aws_principals = [aws_principals]
           for p in aws_principals:
               if ':root' not in p and 'arn:aws:iam' in p:
                   print(f'[INFO] Cross-account principal: {p} | Actions: {actions}')
"
   fi
done

[cta]

Check for S3 Server-Side Encryption Gaps

Encryption at rest is required for most compliance frameworks, but the implementation details matter. SSE-S3 (AES-256) is weaker from a key management perspective than SSE-KMS. Run this to find buckets without KMS-based encryption enforced via bucket policy:

for bucket in $(aws s3api list-buckets --query 'Buckets[*].Name' --output text); do
   enc=$(aws s3api get-bucket-encryption --bucket "$bucket" \
       --query 'ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault.SSEAlgorithm' \
       --output text 2>/dev/null)
   if [ "$enc" = "AES256" ] || [ -z "$enc" ]; then
       echo "[REVIEW] $bucket: encryption is $enc (not KMS-managed)"
   fi
done

[cta]

Buckets holding sensitive data should enforce aws:kms encryption via a bucket policy condition that denies uploads without the correct x-amz-server-side-encryption header set to aws:kms.

EC2 and Instance Metadata: The SSRF Goldmine

Enforce IMDSv2 Across All Instances

The EC2 Instance Metadata Service (IMDS) at 169.254.169.254 has been the backbone of some of the most high-profile cloud breaches. IMDSv1 allows any process on the instance to query the metadata endpoint without authentication, which means an SSRF vulnerability in an application running on that instance translates directly into credential theft.

Check which instances still allow IMDSv1:

aws ec2 describe-instances \
 --query 'Reservations[*].Instances[*].[InstanceId, MetadataOptions.HttpTokens, MetadataOptions.HttpEndpoint, Tags[?Key==`Name`].Value | [0]]' \
 --output table | grep -v "required"

[cta]

Any instance showing optional rather than required for HttpTokens is vulnerable to IMDSv1 abuse. Remediate with:

aws ec2 modify-instance-metadata-options \
 --instance-id i-0123456789abcdef0 \
 --http-tokens required \
 --http-endpoint enabled

[cta]

To enforce this at scale, use a Service Control Policy (SCP) or an AWS Config rule that flags any instance launched without IMDSv2 enforced.

Audit Security Groups for Unrestricted Inbound Rules

This is on every checklist and still gets missed in practice because people audit it once at launch and never again after a developer opens port 22 "temporarily."

aws ec2 describe-security-groups \
 --query 'SecurityGroups[?IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`] || Ipv6Ranges[?CidrIpv6==`::/0`]]].[GroupId, GroupName, IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`]].[FromPort,ToPort]]' \
 --output json | python3 -c "
import json, sys
groups = json.load(sys.stdin)
for g in groups:
   if g[2]:
       for perm in g[2]:
           for port_range in perm:
               print(f'[OPEN] SG: {g[0]} ({g[1]}) Port: {port_range[0]}-{port_range[1]} open to 0.0.0.0/0')
"

[cta]

Pay particular attention to port 22 (SSH), 3389 (RDP), 5432 (PostgreSQL), 3306 (MySQL), and 6379 (Redis). These should never be open to 0.0.0.0/0 in production environments.

Understanding how these open ports get chained with credential theft from IMDS is exactly the kind of attack path covered in the Redfox Cybersecurity Academy AWS Pentesting Course. The course covers lateral movement techniques specific to cloud environments, not just network-level enumeration.

CloudTrail and Logging: The Detection Gaps Attackers Count On

Verify CloudTrail Is Capturing Management Events Across All Regions

A single-region CloudTrail configuration is a common gap. Attackers who know your logging posture will operate in regions you are not monitoring.

aws cloudtrail describe-trails --include-shadow-trails \
 --query 'trailList[*].[Name, IsMultiRegionTrail, IncludeGlobalServiceEvents, HasCustomEventSelectors, HomeRegion]' \
 --output table

[cta]

Look for trails where IsMultiRegionTrail is False. Every production AWS account should have at least one multi-region trail capturing management events and, ideally, data events for S3 and Lambda.

Detect CloudTrail Tampering Attempts

Attackers who gain sufficient IAM permissions often attempt to disable CloudTrail before executing their primary objective. Set up a CloudWatch metric filter and alarm for this:

# Create metric filter for CloudTrail stop/delete events
aws logs put-metric-filter \
 --log-group-name "aws-cloudtrail-logs" \
 --filter-name "CloudTrailChanges" \
 --filter-pattern '{ ($.eventName = CreateTrail) || ($.eventName = UpdateTrail) || ($.eventName = DeleteTrail) || ($.eventName = StartLogging) || ($.eventName = StopLogging) }' \
 --metric-transformations \
   metricName=CloudTrailChanges,metricNamespace=CISBenchmark,metricValue=1

# Create alarm on that metric
aws cloudwatch put-metric-alarm \
 --alarm-name "CloudTrailChangesAlarm" \
 --alarm-description "Alert on CloudTrail modifications" \
 --metric-name CloudTrailChanges \
 --namespace CISBenchmark \
 --statistic Sum \
 --period 300 \
 --threshold 1 \
 --comparison-operator GreaterThanOrEqualToThreshold \
 --evaluation-periods 1 \
 --alarm-actions arn:aws:sns:us-east-1:123456789012:SecurityAlerts

[cta]

Lambda and Serverless: The Forgotten Attack Surface

Audit Lambda Execution Roles for Over-Permissioning

Lambda functions are often created with execution roles that have AdministratorAccess or wildcard s3:* permissions because it was the path of least resistance during development. Enumerate overly permissive Lambda roles:

for fn in $(aws lambda list-functions --query 'Functions[*].FunctionName' --output text); do
   role=$(aws lambda get-function-configuration --function-name "$fn" \
       --query 'Role' --output text)
   role_name=$(echo "$role" | awk -F'/' '{print $NF}')
   echo "=== Lambda: $fn | Role: $role_name ==="
   aws iam list-attached-role-policies --role-name "$role_name" \
       --query 'AttachedPolicies[*].PolicyName' --output text
done

[cta]

If you see AdministratorAccess attached to a Lambda execution role, treat it as a critical finding. An attacker who can invoke that function, or inject code into it via a dependency confusion attack, effectively has full account access.

Check Lambda Environment Variables for Hardcoded Secrets

Hardcoded credentials in Lambda environment variables are trivially extracted by anyone with lambda:GetFunctionConfiguration on the role.

for fn in $(aws lambda list-functions --query 'Functions[*].FunctionName' --output text); do
   envvars=$(aws lambda get-function-configuration --function-name "$fn" \
       --query 'Environment.Variables' --output json 2>/dev/null)
   if [ "$envvars" != "null" ] && [ -n "$envvars" ]; then
       echo "=== $fn ==="
       echo "$envvars" | python3 -c "
import json, sys, re
env = json.load(sys.stdin)
patterns = ['key', 'secret', 'password', 'token', 'credential', 'api_key', 'apikey']
for k, v in env.items():
   if any(p in k.lower() for p in patterns):
       print(f'  [SENSITIVE KEY] {k} = {v[:8]}...')
"
   fi
done

[cta]

All secrets in Lambda should be pulled from AWS Secrets Manager or SSM Parameter Store at runtime, never stored as plaintext environment variables.

GuardDuty and SecurityHub: Misconfigured Detection

Verify GuardDuty Is Enabled in Every Region

GuardDuty operates per-region. It is not uncommon to find it enabled in us-east-1 and nowhere else.

for region in $(aws ec2 describe-regions --query 'Regions[*].RegionName' --output text); do
   status=$(aws guardduty list-detectors --region "$region" \
       --query 'DetectorIds' --output text 2>/dev/null)
   if [ -z "$status" ]; then
       echo "[MISSING] GuardDuty not enabled in: $region"
   else
       detector_status=$(aws guardduty get-detector --detector-id "$status" \
           --region "$region" --query 'Status' --output text 2>/dev/null)
       echo "[$detector_status] $region: Detector $status"
   fi
done

[cta]

This single script will surface every region where your threat detection has a blind spot.

Wrapping Up

The AWS misconfigurations that lead to breaches are not exotic. They are IAM roles with wildcard trust policies, EC2 instances still running IMDSv1, Lambda functions carrying AdministratorAccess, and CloudTrail configurations that cover one region while attackers pivot through five others. These findings show up repeatedly across cloud security assessments because they are easy to create and easy to forget.

The checklist above gives you the commands to surface these issues in your own environment. But knowing what to look for is only half the picture. Understanding how an attacker chains these findings into a full account compromise is what separates a hardened AWS environment from one that is technically compliant but practically vulnerable.

If you want to develop that adversarial perspective through hands-on practice, the AWS Pentesting Course at Redfox Cybersecurity Academy covers IAM privilege escalation, IMDS exploitation, S3 bucket takeover scenarios, and more in a structured lab environment built for cloud security practitioners.

Run the checks. Understand the attack paths. Build defenses that actually hold.

Copy Code