You set up your AWS account months ago. You've been shipping features, not checking security configurations. Somewhere in the back of your mind, you know there are probably forgotten resources running, IAM users without MFA, and security groups open to the world. But who has time to audit an AWS account?
You do. This audit takes 30 minutes and uses only the AWS CLI. Grab a coffee, open your terminal, and work through it step by step. By the end, you'll know exactly where your account stands — and what to fix first.
Before You Start
Make sure you have the AWS CLI installed and configured with credentials that have read access to IAM, EC2, S3, RDS, and billing. If you're using SSO, run aws sso login first.
Part 1: IAM Security (10 minutes)
Check 1: Root Account MFA
aws iam get-account-summary --query 'SummaryMap.AccountMFAEnabled'
If this returns 0, stop everything and enable MFA on your root account right now. This is the single most critical security control for your AWS account.
Check 2: IAM Users Without MFA
aws iam generate-credential-report
sleep 5
aws iam get-credential-report --query 'Content' --output text | base64 -d | \
awk -F',' 'NR>1 && $4=="true" && $8=="false" {print "USER WITHOUT MFA: " $1}'
This finds IAM users who have console access but no MFA enabled. Every user on this list is a credential compromise waiting to happen.
Check 3: Old Access Keys
aws iam get-credential-report --query 'Content' --output text | base64 -d | \
awk -F',' 'NR>1 && $9=="true" {print $1, "Key1 last rotated:", $10}
NR>1 && $14=="true" {print $1, "Key2 last rotated:", $15}'
Any access key older than 90 days should be rotated. Any key older than 180 days should be investigated — there's a good chance nobody remembers what it's for.
Check 4: Users with AdministratorAccess
aws iam list-users --query 'Users[*].UserName' --output text | \
tr '\t' '\n' | while read user; do
aws iam list-attached-user-policies --user-name "$user" \
--query "AttachedPolicies[?PolicyArn=='arn:aws:iam::aws:policy/AdministratorAccess'].PolicyName" \
--output text | grep -q "AdministratorAccess" && echo "ADMIN USER: $user"
done
Fewer admin users is better. Every admin user is a potential full-account compromise.
Part 2: Resource Audit (10 minutes)
Check 5: Running EC2 Instances
aws ec2 describe-instances --filters "Name=instance-state-name,Values=running" \
--query 'Reservations[*].Instances[*].[InstanceId,InstanceType,LaunchTime,Tags[?Key==`Name`].Value|[0]]' \
--output table
Review each running instance. Do you recognize all of them? Is there anything running that was supposed to be temporary? Pay attention to launch dates — anything launched more than 3 months ago that isn't tagged as production deserves scrutiny.
Check 6: Unattached EBS Volumes
aws ec2 describe-volumes --filters "Name=status,Values=available" \
--query 'Volumes[*].[VolumeId,Size,CreateTime]' --output table
Unattached EBS volumes cost money for storage but do nothing useful. These are almost always leftovers from terminated instances. Delete them after confirming you don't need the data (or snapshot them first if you're not sure).
Check 7: Idle RDS Instances
aws rds describe-db-instances \
--query 'DBInstances[*].[DBInstanceIdentifier,DBInstanceClass,DBInstanceStatus,Engine]' \
--output table
Check each RDS instance. Is it in use? Is it the right size? An idle db.r5.xlarge costs over $400/month. If it's a development database that nobody's using, consider stopping or deleting it.
Check 8: Public S3 Buckets
for bucket in $(aws s3api list-buckets --query 'Buckets[*].Name' --output text); do
acl=$(aws s3api get-bucket-acl --bucket "$bucket" --query 'Grants[?Permission==`READ` && Grantee.URI==`http://acs.amazonaws.com/groups/global/AllUsers`]' --output text 2>/dev/null)
[ -n "$acl" ] && echo "PUBLIC BUCKET: $bucket"
done
Any bucket that appears here is accessible to anyone on the internet. Unless this is intentional (a static website bucket, for example), fix it immediately.
Part 3: Cost Review (10 minutes)
Check 9: Top Services by Cost (Last 30 Days)
aws ce get-cost-and-usage \
--time-period Start=$(date -v-30d +%Y-%m-%d),End=$(date +%Y-%m-%d) \
--granularity MONTHLY --metrics UnblendedCost \
--group-by Type=DIMENSION,Key=SERVICE \
--query 'ResultsByTime[0].Groups[*].[Keys[0],Metrics.UnblendedCost.Amount]' \
--output table
Review the top five services by cost. Are there any surprises? A service you don't recognize? Data transfer costs that seem high? This is your cost map — it tells you where your money goes.
Check 10: Elastic IPs Not Attached
aws ec2 describe-addresses --query 'Addresses[?AssociationId==null].[PublicIp,AllocationId]' --output table
AWS charges $0.005/hour ($3.60/month) for Elastic IPs that aren't attached to a running instance. It's small, but it's also the easiest cost to eliminate — release any EIP you're not using.
Your Audit Score
Count how many checks passed without issues. 10/10: your account is in excellent shape. 7-9: you have some cleanup to do but nothing critical. 4-6: block an hour this week to fix the issues you found. Below 4: your account has significant risk — prioritize the IAM findings first.
Making This Automatic
This audit is useful, but it only captures a point-in-time snapshot. By next month, new resources will be running, new IAM users will exist, and configurations will have drifted. Running this manually every month is better than nothing, but it's a commitment that's easy to drop.
Vigilare runs these checks — and hundreds more — automatically, every five minutes. Instead of a monthly CLI session, you get continuous monitoring with a risk score that tells you the moment something needs attention. Start a free 14-day trial.
Related Reading
Protect your AWS accounts before it's too late
Vigilare monitors your AWS accounts for suspension risks — billing anomalies, IAM issues, GuardDuty findings, and more — and alerts you before AWS takes action.
Written by Viktor B.
Co-founder & CEO