AWS Security Checklist: Complete Cloud Infrastructure Hardening Guide
What is the AWS security checklist?
TL;DR
- Bottom line: Secure AWS accounts by locking the root user with MFA, enforcing IAM least-privilege with roles (not long-lived keys), enabling CloudTrail + GuardDuty + Security Hub across all regions, blocking public S3 at the account level, and encrypting everything with KMS.
- Key tool/command:
aws securityhub enable-security-hub --enable-default-standardsenables Security Hub with CIS and FSBP standards; runprowler awsfor a comprehensive open-source audit. - Watch out for: Wildcard IAM policies (
"Action": "*", "Resource": "*") -- the #1 root cause of privilege escalation in AWS. 83% of organizations experienced at least one cloud security incident in 2025, with 23% caused by misconfigurations. - Works with: All AWS regions, all account types (standalone, Organizations). Terraform AWS Provider 5.x, CloudFormation, CDK 2.x. CIS Benchmark v3.0.0, AWS Security Hub FSBP.
Constraints
- NEVER use AWS root account for daily operations -- enable MFA and lock it away
- NEVER embed long-lived access keys in code, environment variables, or config files -- use IAM roles with temporary credentials
- NEVER allow wildcard (*) Actions or Resources in IAM policies for production workloads
- NEVER create S3 buckets without Block Public Access enabled at account level
- CloudTrail MUST be enabled in all regions with log file validation and KMS encryption
- Security Group rules MUST follow least-privilege -- no 0.0.0.0/0 ingress except on port 443 for public-facing load balancers
Quick Reference
| # | AWS Service | Security Control | Risk if Missing | Config/Command |
|---|---|---|---|---|
| 1 | IAM | Enable MFA on root account | Full account takeover | Console > IAM > Root > Assign MFA |
| 2 | IAM | Use IAM roles, not long-lived access keys | Credential leaks, lateral movement | aws iam create-role --assume-role-policy-document |
| 3 | IAM | Enforce least-privilege policies (no wildcards) | Privilege escalation | IAM Access Analyzer + aws accessanalyzer |
| 4 | IAM | Enable IAM Access Analyzer | Undetected public/cross-account access | aws accessanalyzer create-analyzer --type ACCOUNT |
| 5 | S3 | Block Public Access at account level | Data breaches (most common misconfiguration) | aws s3control put-public-access-block --account-id |
| 6 | S3 | Enable default SSE-KMS encryption | Data exposure at rest | aws s3api put-bucket-encryption |
| 7 | S3 | Disable ACLs (use bucket policies only) | Complex, error-prone permissions | BucketOwnerEnforced ownership setting |
| 8 | EC2 | Encrypt all EBS volumes by default | Data exposure if disk stolen/shared | aws ec2 enable-ebs-encryption-by-default |
| 9 | EC2 | Use private subnets for backends | Direct internet exposure | VPC with public/private subnet architecture |
| 10 | RDS | Enable encryption at rest and in transit | Database data exposure | --storage-encrypted --kms-key-id |
| 11 | RDS | Deploy in private subnets, no public access | Database internet exposure | --no-publicly-accessible |
| 12 | Lambda | Use IAM roles with minimal permissions | Over-privileged function compromise | Per-function execution role |
| 13 | Lambda | Store secrets in Secrets Manager, not env vars | Credential exposure in console/logs | aws secretsmanager get-secret-value |
| 14 | VPC | Enable VPC Flow Logs | No network visibility for forensics | aws ec2 create-flow-logs |
| 15 | VPC | Restrict security groups (no 0.0.0.0/0 ingress) | Unauthorized access from internet | aws ec2 describe-security-groups audit |
| 16 | CloudTrail | Enable multi-region trail with log validation | No audit trail for incident response | aws cloudtrail create-trail --is-multi-region |
| 17 | GuardDuty | Enable in all regions | No threat detection | aws guardduty create-detector --enable |
| 18 | Security Hub | Enable with CIS + FSBP standards | No centralized security posture view | aws securityhub enable-security-hub |
| 19 | KMS | Use CMKs with automatic rotation | Non-compliant encryption, manual key management | aws kms enable-key-rotation |
| 20 | Organizations | Use SCPs to block dangerous actions | No preventive guardrails | aws organizations create-policy --type SERVICE_CONTROL_POLICY |
Decision Tree
START: What AWS security area needs attention?
├── Account-level security?
│ ├── YES → Lock root account with MFA, enable Organizations + SCPs, enable Security Hub
│ └── NO ↓
├── Identity & Access (IAM)?
│ ├── YES → Use IAM Identity Center (SSO), roles not keys, least-privilege, Access Analyzer
│ └── NO ↓
├── Data storage (S3/EBS/RDS)?
│ ├── YES → Block Public Access (S3), encrypt at rest (KMS), encrypt in transit (TLS 1.2+)
│ └── NO ↓
├── Compute (EC2/Lambda)?
│ ├── YES → Private subnets, security groups, per-function IAM roles, Secrets Manager
│ └── NO ↓
├── Network (VPC)?
│ ├── YES → VPC Flow Logs, restrict security groups, use VPC endpoints for AWS services
│ └── NO ↓
├── Monitoring & Detection?
│ ├── YES → CloudTrail (all regions), GuardDuty, Security Hub, Config Rules
│ └── NO ↓
├── Compliance audit?
│ ├── YES → Run Prowler, enable CIS Benchmark in Security Hub, AWS Config conformance packs
│ └── NO ↓
└── DEFAULT → Start with IAM + CloudTrail + GuardDuty + S3 Block Public Access (highest impact)
Step-by-Step Guide
1. Lock down the root account
The root account has unrestricted access to all resources. Enable hardware MFA, delete root access keys, and never use root for daily operations. [src1]
# Check if root account has MFA enabled
aws iam get-account-summary --query 'SummaryMap.AccountMFAEnabled'
# Check for root access keys (should return empty)
aws iam list-access-keys --user-name root 2>/dev/null || echo "Use Console to check root keys"
Verify: aws iam get-account-summary --query 'SummaryMap.AccountMFAEnabled' → expected: 1
2. Enforce IAM least-privilege with Access Analyzer
Enable IAM Access Analyzer to detect public and cross-account access, then audit all policies for wildcard permissions. [src2]
# Create IAM Access Analyzer
aws accessanalyzer create-analyzer \
--analyzer-name account-analyzer \
--type ACCOUNT
# List all findings (public/cross-account access)
aws accessanalyzer list-findings \
--analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/account-analyzer
Verify: aws accessanalyzer list-findings → expected: no ACTIVE findings
3. Block public S3 access at account level
S3 is the most commonly misconfigured AWS service. Block public access at the account level. [src5]
# Block public access for entire account
aws s3control put-public-access-block \
--account-id $(aws sts get-caller-identity --query Account --output text) \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
Verify: All four settings return true in output.
4. Enable CloudTrail with KMS encryption
Enable a multi-region trail with log file validation and KMS encryption for tamper-proof audit logging. [src4]
# Create KMS key for CloudTrail encryption
KEY_ID=$(aws kms create-key --description "CloudTrail encryption key" \
--query 'KeyMetadata.KeyId' --output text)
aws kms enable-key-rotation --key-id $KEY_ID
# Create multi-region trail
aws cloudtrail create-trail \
--name organization-trail \
--s3-bucket-name my-cloudtrail-logs \
--is-multi-region-trail \
--enable-log-file-validation \
--kms-key-id $KEY_ID
aws cloudtrail start-logging --name organization-trail
Verify: aws cloudtrail get-trail-status --name organization-trail --query 'IsLogging' → expected: true
5. Enable GuardDuty and Security Hub
GuardDuty provides continuous threat detection; Security Hub aggregates findings into a single dashboard. [src3] [src7]
# Enable GuardDuty
aws guardduty create-detector --enable --finding-publishing-frequency FIFTEEN_MINUTES
# Enable Security Hub with default standards (CIS + FSBP)
aws securityhub enable-security-hub --enable-default-standards
Verify: aws securityhub get-enabled-standards → returns list of enabled standards
6. Encrypt EBS volumes by default
Enable default EBS encryption in every region to ensure all new volumes are automatically encrypted. [src1]
# Enable default EBS encryption in current region
aws ec2 enable-ebs-encryption-by-default
# Verify
aws ec2 get-ebs-encryption-by-default
Verify: aws ec2 get-ebs-encryption-by-default --query 'EbsEncryptionByDefault' → expected: true
7. Run Prowler for comprehensive audit
Prowler executes 240+ security checks based on CIS, NIST, PCI-DSS, HIPAA, and AWS best practices. [src6]
# Install and run Prowler
pip install prowler
prowler aws --compliance cis_3.0_aws -M html
Verify: Review the generated report in output/ directory. Address all FAIL and HIGH severity findings first.
Code Examples
Terraform: Secure IAM Role with Least-Privilege Policy
# Input: AWS account with Terraform configured
# Output: IAM role with minimal S3 read-only access and confused deputy protection
resource "aws_iam_role" "app_role" {
name = "app-read-only"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
Action = "sts:AssumeRole"
Condition = {
StringEquals = { "aws:SourceAccount" = data.aws_caller_identity.current.account_id }
}
}]
})
}
resource "aws_iam_role_policy" "s3_read" {
name = "s3-read-specific-bucket"
role = aws_iam_role.app_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:GetObject", "s3:ListBucket"]
Resource = [
"arn:aws:s3:::my-app-data",
"arn:aws:s3:::my-app-data/*"
]
}]
})
}
Terraform: Secure S3 Bucket with Encryption and Public Access Block
# Input: AWS account with Terraform configured
# Output: S3 bucket with KMS encryption, versioning, and public access block
resource "aws_s3_bucket" "secure_bucket" {
bucket = "my-secure-data-bucket"
}
resource "aws_s3_bucket_versioning" "versioning" {
bucket = aws_s3_bucket.secure_bucket.id
versioning_configuration { status = "Enabled" }
}
resource "aws_s3_bucket_server_side_encryption_configuration" "encryption" {
bucket = aws_s3_bucket.secure_bucket.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.s3_key.arn
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_public_access_block" "block" {
bucket = aws_s3_bucket.secure_bucket.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
CloudFormation: VPC Security Group with Least-Privilege Ingress
# Input: CloudFormation stack deployment
# Output: Security group allowing only HTTPS from corporate network
Resources:
WebServerSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "HTTPS only from corporate network"
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 10.0.0.0/8
Description: "HTTPS from corporate"
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: "HTTPS outbound for API calls"
Terraform: Lambda with Minimal IAM and VPC Isolation
# Input: AWS account with VPC and RDS configured
# Output: Lambda function in private subnet with Secrets Manager access
resource "aws_iam_role" "lambda_role" {
name = "lambda-db-reader"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "lambda_policy" {
name = "lambda-minimal-access"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = [aws_secretsmanager_secret.db_creds.arn]
},
{
Effect = "Allow"
Action = ["rds-db:connect"]
Resource = ["arn:aws:rds-db:*:*:dbuser:*/lambda_user"]
}
]
})
}
Anti-Patterns
Wrong: Wildcard IAM policy
// BAD -- grants full admin access to everything
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}]
}
Correct: Scoped IAM policy with specific actions and resources
// GOOD -- only the permissions the application actually needs
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-app-bucket/*"
}]
}
Wrong: S3 bucket with public ACL
// BAD -- makes bucket contents publicly readable
resource "aws_s3_bucket_acl" "public" {
bucket = aws_s3_bucket.data.id
acl = "public-read"
}
Correct: S3 bucket with public access blocked
// GOOD -- blocks all public access at bucket level
resource "aws_s3_bucket_public_access_block" "block" {
bucket = aws_s3_bucket.data.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
Wrong: Security group open to the world
// BAD -- allows SSH from any IP address
resource "aws_security_group_rule" "ssh" {
type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.server.id
}
Correct: Security group restricted to specific sources
// GOOD -- SSH only from bastion host security group
resource "aws_security_group_rule" "ssh" {
type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
source_security_group_id = aws_security_group.bastion.id
security_group_id = aws_security_group.server.id
}
Wrong: Hardcoded credentials in Lambda environment variables
# BAD -- credentials visible in Lambda console, CloudWatch logs, API responses
import os
DB_PASSWORD = os.environ['DB_PASSWORD'] # Stored as Lambda env var
Correct: Retrieve secrets from AWS Secrets Manager at runtime
# GOOD -- secrets never stored in code or environment variables
import boto3
import json
def get_db_credentials():
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId='prod/db/credentials')
return json.loads(response['SecretString'])
Common Pitfalls
- Root account used for daily work: Root has unlimited access and cannot be restricted by IAM policies or SCPs. Fix: Create IAM users/roles, enable root MFA, delete root access keys. [src1]
- Long-lived access keys in code repos: Access keys committed to Git are scraped by bots within minutes. Fix: Use
git-secretspre-commit hook, rotate keys immediately if exposed, switch to IAM roles. [src2] - S3 bucket policies overriding account-level Block Public Access: Individual bucket policies can still grant access if Block Public Access is only set at the bucket level. Fix: Enable Block Public Access at the account level. [src5]
- CloudTrail only in one region: Attackers operate in regions you are not monitoring. Fix: Create a multi-region trail with
--is-multi-region-trail. [src4] - Security groups with 0.0.0.0/0 on SSH (port 22): Most common initial access vector for EC2 compromise. Fix: Restrict SSH to bastion host security group or use Systems Manager Session Manager. [src3]
- RDS publicly accessible by default: Some Terraform defaults may leave
publicly_accessible = true. Fix: Explicitly set--no-publicly-accessibleand deploy in private subnets. [src2] - Lambda environment variables for secrets: Environment variables are visible in the Lambda console and may appear in logs. Fix: Use Secrets Manager with
GetSecretValueat runtime. [src3] - KMS keys without rotation: Non-rotated keys increase the blast radius of a compromised key. Fix: Enable automatic rotation with
aws kms enable-key-rotation. [src1]
Diagnostic Commands
# Run comprehensive Prowler security assessment
prowler aws --compliance cis_3.0_aws -M json html
# Check IAM credential report (users, MFA, key age)
aws iam generate-credential-report && sleep 5
aws iam get-credential-report --query 'Content' --output text | base64 -d
# Check S3 Block Public Access at account level
aws s3control get-public-access-block \
--account-id $(aws sts get-caller-identity --query Account --output text)
# Check CloudTrail status
aws cloudtrail describe-trails \
--query 'trailList[*].{Name:Name,Multi:IsMultiRegionTrail,Log:LogFileValidationEnabled}'
# List GuardDuty detectors
aws guardduty list-detectors
# Find security groups with 0.0.0.0/0 ingress
aws ec2 describe-security-groups \
--filters Name=ip-permission.cidr,Values=0.0.0.0/0 \
--query 'SecurityGroups[*].{ID:GroupId,Name:GroupName}'
# Check for publicly accessible RDS instances
aws rds describe-db-instances \
--query 'DBInstances[?PubliclyAccessible==`true`].DBInstanceIdentifier'
# Check EBS default encryption
aws ec2 get-ebs-encryption-by-default
# Audit VPC Flow Logs
aws ec2 describe-flow-logs --query 'FlowLogs[*].{VPC:ResourceId,Status:FlowLogStatus}'
Version History & Compatibility
| Standard/Tool | Version | Status | Key Changes |
|---|---|---|---|
| CIS AWS Foundations Benchmark | v3.0.0 | Current | Added IAM Access Analyzer checks, expanded S3 controls |
| CIS AWS Foundations Benchmark | v2.0.0 | Supported | Restructured sections, added Organizations controls |
| AWS Security Hub FSBP | 2025 | Current | 250+ controls across 30+ services |
| Prowler | 4.x | Current | Multi-cloud (AWS/Azure/GCP), 240+ AWS checks |
| Prowler | 3.x | Maintained | AWS-only, CIS v2.0 baseline |
| AWS Well-Architected Security Pillar | 2025 | Current | Updated identity, detection, and response guidance |
| Terraform AWS Provider | 5.x | Current | S3 bucket resources split into sub-resources |
| Terraform AWS Provider | 4.x | Maintained | Unified S3 bucket resource |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Setting up a new AWS account or organization | Using Azure, GCP, or multi-cloud | Cloud-specific security checklists |
| Preparing for a security audit (SOC 2, ISO 27001) | Need application-layer security (OWASP Top 10) | XSS/SQLi/CSRF prevention guides |
| Remediating Security Hub or Prowler findings | Need container/Kubernetes security | EKS security best practices |
| Hardening an existing AWS environment | Need compliance-specific controls (HIPAA, PCI) | AWS compliance-specific guides |
| Onboarding developers to AWS security practices | Managing on-premises infrastructure | On-prem security hardening guides |
Important Caveats
- This checklist covers the most critical controls but is not exhaustive -- AWS has 200+ services, each with its own security surface. Use Security Hub and Prowler for comprehensive coverage.
- CIS Benchmark Level 1 vs Level 2: Level 1 controls are essential for all accounts. Level 2 adds stricter controls for regulated environments but may impact functionality.
- AWS default settings have improved significantly (S3 Block Public Access, SSE-S3 default encryption for new buckets since Jan 2023) but older accounts may not have these defaults applied.
- Terraform AWS Provider 5.x changed S3 bucket configuration to separate resources. Check your provider version before applying examples.
- SCPs in AWS Organizations affect all accounts in the OU but never restrict the management account itself.
- GuardDuty and Security Hub must be enabled per-region. Use Organizations delegated administrator feature to enable across all accounts and regions automatically.