AWS security audit checklist: a practical guide for DevOps teams
AWS environment security audit procedure, from IAM review, through network configuration, to encryption and monitoring. A ready-to-use checklist for recurring assessments.
If you need external support running a cloud security audit, check our DevOps and cloud security consulting service.
When to run an audit
- Before migrating production to a new AWS account
- After a security incident or suspected breach
- On a quarterly cycle (every six months at minimum)
- Before a compliance audit (SOC2, ISO 27001, NIS2)
- After significant infrastructure changes (new VPC, new account in the organisation)
Automation tools
Before you start a manual review, run automated scanners:
# AWS Security Hub - native compliance check
aws securityhub get-findings --filters '{"RecordState":[{"Value":"ACTIVE","Comparison":"EQUALS"}]}' \
--query 'Findings[?Severity.Label==`CRITICAL`].[Title,Resources[0].Id]' --output table
# Prowler - open-source AWS audit (install: pip install prowler)
prowler aws --severity critical high --output-formats json-ocsf html
# ScoutSuite - multi-cloud security auditing
scout aws --report-dir ./audit-$(date +%Y%m%d)
# AWS Trusted Advisor (requires Business/Enterprise support)
aws support describe-trusted-advisor-checks --language en \
--query 'checks[?category==`security`].name'
Prowler covers over 300 checks from CIS AWS Benchmark, PCI-DSS, and GDPR. If you can only run one tool, pick Prowler.
Checklist: IAM and access
Root account
# Check if root has API keys (it should NOT)
aws iam get-account-summary --query 'SummaryMap.AccountAccessKeysPresent'
# Expected result: 0
# Check if MFA is enabled on root
aws iam get-account-summary --query 'SummaryMap.AccountMFAEnabled'
# Expected result: 1
| Checkpoint | Expected state | How to check |
|---|---|---|
| Root account has no API keys | AccountAccessKeysPresent = 0 | aws iam get-account-summary |
| MFA on root account | AccountMFAEnabled = 1 | aws iam get-account-summary |
| Root account unused for >90 days | No recent login | Credential Report |
| MFA on all IAM users with console access | 100% coverage | aws iam generate-credential-report |
| No inline policies on users | All through groups/roles | prowler aws -c iam_inline_policy_no_administrative_privileges |
| API key rotation <90 days | No stale keys | Credential Report column access_key_last_rotated |
Policies and roles
# Find users with AdministratorAccess
aws iam list-entities-for-policy \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
--query '[PolicyUsers[].UserName, PolicyGroups[].GroupName, PolicyRoles[].RoleName]'
# Find policies with wildcard (*) on actions and resources
aws iam list-policies --scope Local --query 'Policies[].Arn' --output text | \
xargs -I {} aws iam get-policy-version --policy-arn {} \
--version-id $(aws iam get-policy --policy-arn {} --query 'Policy.DefaultVersionId' --output text) \
--query 'PolicyVersion.Document'
# Unused roles (no assume in last 90 days)
aws iam generate-service-last-accessed-details --arn arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME
Checklist: Network and access
# Security Groups with open 0.0.0.0/0 on ports other than 80/443
aws ec2 describe-security-groups \
--filters Name=ip-permission.cidr,Values='0.0.0.0/0' \
--query 'SecurityGroups[].{Name:GroupName,ID:GroupId,Rules:IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`]].{Port:FromPort,Proto:IpProtocol}}' \
--output table
# Public subnets without NAT Gateway (potential attack vector)
aws ec2 describe-route-tables --query 'RouteTables[].Routes[?GatewayId!=null && DestinationCidrBlock==`0.0.0.0/0`]'
# VPC Flow Logs - check if enabled on all VPCs
aws ec2 describe-flow-logs --query 'FlowLogs[].{VPC:ResourceId,Status:FlowLogStatus,Dest:LogDestination}'
| Checkpoint | Expected state |
|---|---|
| No SG with 0.0.0.0/0 on SSH (22) or RDP (3389) | Zero results |
| VPC Flow Logs enabled on every VPC | 100% |
| No public IPs on database instances | Zero results |
| NACLs blocking known malicious IP ranges | Configured |
| No default VPC in use for production | Deleted or empty |
Checklist: Encryption and storage
# S3 buckets without default encryption
aws s3api list-buckets --query 'Buckets[].Name' --output text | tr '\t' '\n' | while read bucket; do
enc=$(aws s3api get-bucket-encryption --bucket "$bucket" 2>&1)
if echo "$enc" | grep -q "ServerSideEncryptionConfigurationNotFoundError"; then
echo "NO ENCRYPTION: $bucket"
fi
done
# S3 buckets with public access
aws s3api list-buckets --query 'Buckets[].Name' --output text | tr '\t' '\n' | while read bucket; do
status=$(aws s3api get-public-access-block --bucket "$bucket" 2>&1)
if echo "$status" | grep -q "NoSuchPublicAccessBlockConfiguration"; then
echo "NO PUBLIC ACCESS BLOCK: $bucket"
fi
done
# EBS volumes without encryption
aws ec2 describe-volumes --filters Name=encrypted,Values=false \
--query 'Volumes[].{ID:VolumeId,Size:Size,State:State}' --output table
# RDS without at-rest encryption
aws rds describe-db-instances \
--query 'DBInstances[?StorageEncrypted==`false`].{ID:DBInstanceIdentifier,Engine:Engine}' --output table
Checklist: Logging and monitoring
# CloudTrail - check if active and multi-region
aws cloudtrail describe-trails --query 'trailList[].{Name:Name,MultiRegion:IsMultiRegionTrail,Logging:HasCustomEventSelectors}'
aws cloudtrail get-trail-status --name default --query '{IsLogging:IsLogging,LatestDelivery:LatestDeliveryTime}'
# Config - is AWS Config recording
aws configservice describe-configuration-recorders \
--query 'ConfigurationRecorders[].{Name:name,Recording:recording}'
# GuardDuty - is it enabled
aws guardduty list-detectors --query 'DetectorIds'
# Alarms on key events (root login, IAM policy changes)
aws cloudwatch describe-alarms --alarm-name-prefix "Security-" \
--query 'MetricAlarms[].{Name:AlarmName,State:StateValue}'
| Checkpoint | Expected state |
|---|---|
| CloudTrail active, multi-region | IsLogging: true, Multi-region: true |
| CloudTrail logs in a separate account (log archive) | Bucket in a different account |
| AWS Config recording changes | Recording: true |
| GuardDuty enabled in every region | DetectorId in each region |
| Alarm on root login | Configured and active |
| Alarm on CreateUser/AttachPolicy | Configured and active |
Reporting results
After running Prowler, generate a report:
# Full report with prioritization
prowler aws --severity critical high medium \
--output-formats html csv json-ocsf \
--output-directory ./audit-report-$(date +%Y%m%d)
# Compare with previous audit
prowler aws --severity critical high \
--output-formats csv \
| diff previous-audit.csv - > delta-findings.txt
The report should categorize findings:
- Critical - fix within 24 hours (open root keys, public databases)
- High - fix within a week (missing MFA, missing encryption)
- Medium - plan for next sprint (key rotation, unused roles)
Automating recurring audits
Schedule the audit as a recurring pipeline (e.g., weekly in CI/CD):
# .github/workflows/security-audit.yml
name: Weekly AWS Security Audit
on:
schedule:
- cron: '0 6 * * 1' # Monday 6:00 UTC
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Prowler
run: pip install prowler
- name: Run audit
run: prowler aws --severity critical high --output-formats json-ocsf html
env:
AWS_ACCESS_KEY_ID: $
AWS_SECRET_ACCESS_KEY: $
AWS_DEFAULT_REGION: eu-central-1
- name: Upload report
uses: actions/upload-artifact@v4
with:
name: security-audit-$
path: output/
The IAM role for auditing should have the SecurityAudit managed policy + ViewOnlyAccess, no write permissions.
Next steps
- Fix critical findings immediately
- Implement AWS Config Rules for continuous monitoring
- Consider AWS Organisations with SCPs (Service Control Policies) as guardrails
- Set up Security Hub with automated findings aggregation
If you need help running an audit or implementing security automation, get in touch.