AWS Security Checklist: Complete Cloud Infrastructure Hardening Guide

Type: Software Reference Confidence: 0.93 Sources: 7 Verified: 2026-02-27 Freshness: 2026-02-27

TL;DR

Constraints

Quick Reference

#AWS ServiceSecurity ControlRisk if MissingConfig/Command
1IAMEnable MFA on root accountFull account takeoverConsole > IAM > Root > Assign MFA
2IAMUse IAM roles, not long-lived access keysCredential leaks, lateral movementaws iam create-role --assume-role-policy-document
3IAMEnforce least-privilege policies (no wildcards)Privilege escalationIAM Access Analyzer + aws accessanalyzer
4IAMEnable IAM Access AnalyzerUndetected public/cross-account accessaws accessanalyzer create-analyzer --type ACCOUNT
5S3Block Public Access at account levelData breaches (most common misconfiguration)aws s3control put-public-access-block --account-id
6S3Enable default SSE-KMS encryptionData exposure at restaws s3api put-bucket-encryption
7S3Disable ACLs (use bucket policies only)Complex, error-prone permissionsBucketOwnerEnforced ownership setting
8EC2Encrypt all EBS volumes by defaultData exposure if disk stolen/sharedaws ec2 enable-ebs-encryption-by-default
9EC2Use private subnets for backendsDirect internet exposureVPC with public/private subnet architecture
10RDSEnable encryption at rest and in transitDatabase data exposure--storage-encrypted --kms-key-id
11RDSDeploy in private subnets, no public accessDatabase internet exposure--no-publicly-accessible
12LambdaUse IAM roles with minimal permissionsOver-privileged function compromisePer-function execution role
13LambdaStore secrets in Secrets Manager, not env varsCredential exposure in console/logsaws secretsmanager get-secret-value
14VPCEnable VPC Flow LogsNo network visibility for forensicsaws ec2 create-flow-logs
15VPCRestrict security groups (no 0.0.0.0/0 ingress)Unauthorized access from internetaws ec2 describe-security-groups audit
16CloudTrailEnable multi-region trail with log validationNo audit trail for incident responseaws cloudtrail create-trail --is-multi-region
17GuardDutyEnable in all regionsNo threat detectionaws guardduty create-detector --enable
18Security HubEnable with CIS + FSBP standardsNo centralized security posture viewaws securityhub enable-security-hub
19KMSUse CMKs with automatic rotationNon-compliant encryption, manual key managementaws kms enable-key-rotation
20OrganizationsUse SCPs to block dangerous actionsNo preventive guardrailsaws 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

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/ToolVersionStatusKey Changes
CIS AWS Foundations Benchmarkv3.0.0CurrentAdded IAM Access Analyzer checks, expanded S3 controls
CIS AWS Foundations Benchmarkv2.0.0SupportedRestructured sections, added Organizations controls
AWS Security Hub FSBP2025Current250+ controls across 30+ services
Prowler4.xCurrentMulti-cloud (AWS/Azure/GCP), 240+ AWS checks
Prowler3.xMaintainedAWS-only, CIS v2.0 baseline
AWS Well-Architected Security Pillar2025CurrentUpdated identity, detection, and response guidance
Terraform AWS Provider5.xCurrentS3 bucket resources split into sub-resources
Terraform AWS Provider4.xMaintainedUnified S3 bucket resource

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Setting up a new AWS account or organizationUsing Azure, GCP, or multi-cloudCloud-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 findingsNeed container/Kubernetes securityEKS security best practices
Hardening an existing AWS environmentNeed compliance-specific controls (HIPAA, PCI)AWS compliance-specific guides
Onboarding developers to AWS security practicesManaging on-premises infrastructureOn-prem security hardening guides

Important Caveats

Related Units